Repository: lusakasa/saka Branch: master Commit: 97e1d6fbd2c6 Files: 114 Total size: 181.7 KB Directory structure: gitextract_snas81cp/ ├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── .vscode/ │ └── launch.json ├── LICENSE ├── README.md ├── docs/ │ └── pull_request_template.md ├── images/ │ └── favicons/ │ ├── browserconfig.xml │ └── manifest.json ├── jest.config.js ├── jest.transform.js ├── manifest/ │ ├── chrome.json │ ├── common.json │ └── firefox.json ├── package.json ├── spec/ │ └── support/ │ └── jasmine.json ├── src/ │ ├── background_page/ │ │ ├── index.js │ │ └── tabHistory.js │ ├── content_script/ │ │ └── toggle_saka.js │ ├── lib/ │ │ ├── colors.js │ │ ├── dom.js │ │ ├── highlight.jsx │ │ ├── log.js │ │ ├── tld.js │ │ ├── trie.js │ │ ├── url.js │ │ └── utils.js │ ├── msg/ │ │ ├── client.js │ │ └── server.js │ ├── options/ │ │ ├── Main/ │ │ │ ├── MainOptions.jsx │ │ │ ├── OptionsList/ │ │ │ │ ├── DefaultModeSelection.jsx │ │ │ │ ├── EnableFuzzySearch.jsx │ │ │ │ ├── OnlyShowSearchBarSelector.jsx │ │ │ │ ├── ShowSakaHotkeys.jsx │ │ │ │ └── index.jsx │ │ │ └── SakaHotkeysList/ │ │ │ ├── HotkeyListRow.jsx │ │ │ └── index.jsx │ │ └── saka-options.jsx │ ├── saka/ │ │ ├── Main/ │ │ │ ├── Components/ │ │ │ │ ├── BackgroundImage/ │ │ │ │ │ └── index.jsx │ │ │ │ ├── GUIContainer/ │ │ │ │ │ └── index.jsx │ │ │ │ ├── Icon/ │ │ │ │ │ └── index.jsx │ │ │ │ ├── ModeSwitcher/ │ │ │ │ │ └── index.jsx │ │ │ │ ├── PaginationBar/ │ │ │ │ │ └── index.jsx │ │ │ │ ├── SearchBar/ │ │ │ │ │ ├── Button/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── Input/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ └── index.jsx │ │ │ │ ├── SettingsBar/ │ │ │ │ │ └── index.jsx │ │ │ │ └── SuggestionList/ │ │ │ │ ├── Components/ │ │ │ │ │ └── Suggestion/ │ │ │ │ │ └── index.jsx │ │ │ │ ├── Containers/ │ │ │ │ │ ├── BookmarkSuggestion/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── ClosedTabSuggestion/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── CommandSuggestion/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── HistorySuggestion/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── RecentlyViewedSuggestion/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── SearchEngineSuggestion/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── SuggestionSelector.jsx │ │ │ │ │ ├── TabSuggestion/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ └── UnknownSuggestion/ │ │ │ │ │ └── index.jsx │ │ │ │ └── index.jsx │ │ │ ├── Containers/ │ │ │ │ ├── GeneralSearch/ │ │ │ │ │ └── index.jsx │ │ │ │ ├── StandardSearch/ │ │ │ │ │ └── index.jsx │ │ │ │ └── TabSearch/ │ │ │ │ └── index.jsx │ │ │ └── index.jsx │ │ └── index.jsx │ ├── scss/ │ │ ├── options.scss │ │ └── styles.scss │ ├── suggestion_engine/ │ │ ├── client/ │ │ │ └── index.js │ │ └── server/ │ │ ├── index.js │ │ └── providers/ │ │ ├── bookmark.js │ │ ├── closedTab.js │ │ ├── command.js │ │ ├── history.js │ │ ├── index.js │ │ ├── mode.js │ │ ├── recentlyViewed.js │ │ ├── searchEngine.js │ │ └── tab.js │ └── suggestion_utils/ │ └── index.js ├── static/ │ ├── background_page.html │ ├── material-icons.css │ ├── options.html │ └── saka.html ├── test/ │ ├── Icon.test.js │ ├── Main.test.js │ ├── ModeSwitcher.test.js │ ├── PaginationBar.test.js │ ├── SearchBar.test.js │ ├── StandardSearch/ │ │ └── StandardSearch.test.js │ ├── Suggestion.test.js │ ├── SuggestionList.test.js │ ├── __mocks__/ │ │ ├── browser-mocks.js │ │ └── styleMock.scss │ ├── __snapshots__/ │ │ ├── ModeSwitcher.test.js.snap │ │ ├── SearchBar.test.js.snap │ │ ├── Suggestion.test.js.snap │ │ └── SuggestionList.test.js.snap │ ├── lib/ │ │ ├── hightlight.test.js │ │ ├── log.test.js │ │ ├── url.test.js │ │ └── utils.test.js │ ├── options/ │ │ ├── MainOptions.test.js │ │ └── __snapshots__/ │ │ └── MainOptions.test.js.snap │ ├── suggestion_engine/ │ │ ├── providers/ │ │ │ ├── bookmark.test.js │ │ │ ├── closedTab.test.js │ │ │ ├── history.test.js │ │ │ ├── mode.test.js │ │ │ ├── recentlyViewed.test.js │ │ │ └── tab.test.js │ │ └── server/ │ │ └── index.test.js │ └── suggestion_utils/ │ └── index.test.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "sourceMaps": true, "plugins": [ ["transform-react-jsx", { "pragma": "h" }], ["transform-object-rest-spread"], ["transform-class-properties"] ] } ================================================ FILE: .eslintignore ================================================ test/* ================================================ FILE: .eslintrc.json ================================================ { "parser": "babel-eslint", "plugins": ["prettier"], "extends": ["airbnb", "prettier"], "rules": { "react/react-in-jsx-scope": "off", "import/no-extraneous-dependencies": "off", "react/prop-types": "off", "react/no-did-mount-set-state": "off", "jsx-a11y/no-noninteractive-element-interactions": "off", "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], "no-prototype-builtins": "off" }, "env": { "webextensions": true, "browser": true }, "globals": { "document": true, "navigator": false, "window": true, "SAKA_DEBUG": true, "SAKA_PLATFORM": true, "SAKA_VERSION": true, "SAKA_BENCHMARK": true }, "settings": { "import/resolver": { "node": { "paths": ["src", "lib", "msg"], "extensions": ["js", "jsx"] } } } } ================================================ FILE: .gitignore ================================================ dist dist.zip dist.crx dist.pem node_modules npm-debug.log debug.log .DS_Store yarn-error.log coverage saka-*.zip package-lock.json ================================================ FILE: .prettierignore ================================================ package.json ================================================ FILE: .prettierrc ================================================ { "singleQuote": true } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - lts/* install: - yarn global add codecov - yarn install script: - yarn test --coverage && codecov # auto-release from tagged commits before_deploy: - yarn run build:chrome - yarn run zip saka-chrome-dev.zip dist/* - yarn run build:chrome:prod - yarn run zip saka-chrome-prod.zip dist/* - yarn run build:firefox - yarn run zip saka-firefox-dev.zip dist/* - yarn run build:firefox:prod - yarn run zip saka-firefox-prod.zip dist/* deploy: provider: releases api_key: secure: UXpxguYgvL0clkrmjZw9KtLBgRCbXALVi4uz4nU7j96Qcobps2Ui5JFNJc243x2K0oHENYF3GN72j7zw3cclDeGL4/KXa/gOwM/LpHnAD3kJnKlDuVpUTARbp+dm8C2AvcwjtDT/qMY4L+wmfdUEaHKw3DHDJGVWnTEeC35CQ/uAzQJuMd8QGHSUNylY3pcBY2FPXMWaX9JlW7lHjTQW4CY/kBENKmloPrdB2WlnWXlNw7RgkqTk75zLPxUK3VyNW8qH3yInj0vlDD2mfQb4e2s+OjipcGK7Z82fqz4tLEjvi5w/9vIHv3WELF3W36J47LowWUskc+ET3VXULFa2+eMoxtpfyc07KNEVNN9oiEe7jh3zYmvaooq2baBmePBnntmZVPe3zggTvbn7fjqUnp+4oP+KatuPTeuR9QWRvehhO+CftN5m2q66D+L+M4HBQK4SwVt+JR59DMmDja/CfB72Zsz4nSTyFOLRieJLyiM+z4gcCdF78nhKywxrllAmSp+wixW0fnGoTRMyE1YchxSg/uwxvCTQhVZnnIKjm4lxD73VNURsPI7HOyvcy+kxkscB+E6glUQykJfCLdrZZaUa8doVWBI+XNR9IGPhOr+p5jT+w8BRFdwc7VPp4hRsIcWDKXDUMLasyEmQB0H9bgPw27d5R7Lkammud5Sv0Kw= file: - saka-chrome-dev.zip - saka-chrome-prod.zip - saka-firefox-dev.zip - saka-firefox-prod.zip skip_cleanup: true on: repo: lusakasa/saka branch: master tags: true ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Debug Jest Tests", "type": "node", "request": "launch", "runtimeArgs": [ "--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand" ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229, "skipFiles": ["/**"] } ] } ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Sufyan Dawoodjee, Uzair Shamim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Saka [![GitHub license](https://img.shields.io/github/license/lusakasa/saka.svg)](https://github.com/lusakasa/saka/blob/master/LICENSE) [![Build Status](https://travis-ci.org/lusakasa/saka.svg?branch=master&style=popout-square)](https://travis-ci.org/lusakasa/saka) [![codecov.io Code Coverage](https://codecov.io/gh/lusakasa/saka/branch/master/graph/badge.svg?maxAge=2592000)](https://codecov.io/github/lusakasa/saka?branch=master) [![Chrome Web Store](https://img.shields.io/chrome-web-store/stars/nbdfpcokndmapcollfpjdpjlabnibjdi.svg)](https://chrome.google.com/webstore/detail/saka/nbdfpcokndmapcollfpjdpjlabnibjdi) A browsing assistant for [Firefox](https://addons.mozilla.org/firefox/addon/saka/) and [Chrome](https://chrome.google.com/webstore/detail/saka/nbdfpcokndmapcollfpjdpjlabnibjdi) designed to be fast, intuitive, and beautiful. Inspired by Spotlight. Keyboard-focused but mouse friendly too. * Lists tabs in order of recency by default, then fuzzy search by title or URL. * Search recently closed tabs * Search all bookmarks * Search all browsing history * Search all modes at once ## Install Install Saka from the [Firefox Marketplace](https://addons.mozilla.org/firefox/addon/saka/) or [Chrome Webstore](https://chrome.google.com/webstore/detail/saka/nbdfpcokndmapcollfpjdpjlabnibjdi). ## Development See the [Getting Started](https://github.com/lusakasa/saka/wiki/Getting-Started) page on the project Wiki. ## Release Instructions (for maintainers) 1. Update the version number in `manifest/common.json` 2. Make a commit and set the message to the version: `git commit -m "v0.15.2"` 3. Tag the commit with the version and a message describing changes since the last release: `git tag -a v0.15.2` 4. Push the commit to github with tags: `git push origin --follow-tags` 5. View the build status at https://travis-ci.org/lusakasa/saka/ and generated releases at https://github.com/lusakasa/saka/releases ## License MIT Licensed, Copyright (c) 2017 Sufyan Dawoodjee, Uzair Shamim ================================================ FILE: docs/pull_request_template.md ================================================ ## Type of Change > Put an [x] for the relevant option - [ ] Bugfix/Cleanup (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) ## Summary Of Changes #### Why is this change needed? #### Does this close any open issues? ## Checklist - [ ] Tests are passing locally - [ ] Updated the README/Wiki documentation (if relevant) ## Additional Comments Add any additional comments you may have here. ================================================ FILE: images/favicons/browserconfig.xml ================================================ #da532c ================================================ FILE: images/favicons/manifest.json ================================================ { "name": "", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: jest.config.js ================================================ module.exports = { moduleNameMapper: { '@/(.*)$': '/src/$1', '^src/(.*)$': '/src/$1', '^suggestion_utils/(.*)$': '/src/suggestion_utils/$1', '^suggestion_engine/(.*)$': '/src/suggestion_engine/$1', '^lib/(.*)$': '/src/lib/$1', '^msg/(.*)$': '/src/msg/$1', '^.*\\.(css|less|sass|scss)$': '/test/__mocks__/styleMock.scss', '^react-dom/server$': '/node_modules/preact-render-to-string/dist/index.js', '^react-addons-test-utils$': '/node_modules/preact-test-utils/lib/index.js', '^react$': '/node_modules/preact-compat/lib/index.js', '^react-dom$': '/node_modules/preact-compat/lib/index.js' }, moduleFileExtensions: ['js', 'jsx'], transform: { '^.+\\.(js|jsx)?$': '/jest.transform.js' }, collectCoverage: true, collectCoverageFrom: ['src/**/*.{js,jsx}'], coverageThreshold: { global: { branches: 59, functions: 65, lines: 62 } }, coverageReporters: ['json', 'lcov', 'html'], setupFiles: ['/test/__mocks__/browser-mocks.js'] }; ================================================ FILE: jest.transform.js ================================================ const babelOptions = { presets: [['env', { targets: { node: '8' } }], 'react'], plugins: [ [ 'transform-react-jsx', { pragma: 'h' } ] ] }; module.exports = require('babel-jest').createTransformer(babelOptions); ================================================ FILE: manifest/chrome.json ================================================ { "background": { "page": "background_page.html", "persistent": true }, "commands": { "toggleSaka4": { "description": "Toggle Saka", "global": true } }, "permissions": ["chrome://favicon/"], "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsgFNwezxiHxK/AIKh9K4LVefUPIDisRzQLmmRBjFftr3yrxJuLBaO1LvMISsRlvbFmbZ0B6KHzyGDEnmOMIo3X9wUxBwSV+q3kigp3XFryllhOHbKcl+FUVMRQabBs49CS6ttq2l4Jx+ULEsmprqub+IV+cvY3slXxxuyMF0TjmnRwDYnSREgbfzK7g/msZ8e0gi+hrPETNVKOibbVwd8S7gSy0hDug71WOA338FGe08DiXpnVoKLL0fmd+TO6Z4fAhH+oSqr15WIt6qCglYmZi2BEydL0zhUcidkis0zJjNX5nQi0xzggmxwsMJyfmfGoniwsUg3rAy+M0v5gaSUQIDAQAB" } ================================================ FILE: manifest/common.json ================================================ { "name": "Saka", "version": "0.17.3", "author": "Sufyan Dawoodjee, Uzair Shamim", "description": "Saka - elegent tab search, selection, and beyond", "manifest_version": 2, "options_ui": { "page": "options.html", "open_in_tab": true }, "background": { "page": "background_page.html" }, "commands": { "toggleSaka": { "suggested_key": { "default": "Ctrl+Space", "mac": "Alt+Space" }, "description": "Toggle Saka" }, "toggleSaka2": { "suggested_key": { "default": "Ctrl+E", "mac": "Command+E" }, "description": "Toggle Saka" }, "toggleSaka3": { "description": "Toggle Saka" }, "toggleSaka4": { "suggested_key": { "default": "Ctrl+Shift+1" }, "description": "Toggle Saka" } }, "browser_action": { "default_icon": { "48": "logo.png" } }, "icons": { "16": "logo.png", "48": "logo.png", "128": "logo.png" }, "permissions": [ "", "tabs", "sessions", "history", "bookmarks", "storage", "contextMenus" ], "web_accessible_resources": [ "saka.html", "material-icons.css", "MaterialIcons-Regular.woff2" ] } ================================================ FILE: manifest/firefox.json ================================================ { "applications": { "gecko": { "id": "{7d7cad35-2182-4457-972d-5a41a2051240}" } } } ================================================ FILE: package.json ================================================ { "name": "saka", "description": "A keyboard interface to the web", "scripts": { "build": "echo \"You must specify the target browser (firefox or chrome). Example: npm run build:firefox\"", "build:prod": "echo \"You must specify the target browser (firefox or chrome). Example: npm run build:firefox:prod\"", "build:chrome": "npm run clean && webpack --config webpack.config.js --env=dev:chrome:benchmark --progress --colors", "build:chrome:prod": "npm run clean && webpack --config webpack.config.js --env=prod:chrome:nobenchmark --progress --colors", "build:firefox": "npm run clean && webpack --config webpack.config.js --env=dev:firefox:benchmark --progress --colors", "build:firefox:prod": "npm run clean && webpack --config webpack.config.js --env=prod:firefox:nobenchmark --progress --colors", "build:profile": "npm run clean && webpack --config webpack.config.js --progress --profile --colors && cp ./static/* ./dist", "zip": "bestzip", "clean": "rimraf dist", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage --watch --verbose false", "storybook": "start-storybook -p 9001 -c .storybook" }, "repository": { "type": "git", "url": "git+https://github.com/lusakasa/saka.git" }, "license": "MIT", "devDependencies": { "@material/ripple": "^0.38.1", "@types/chrome": "^0.0.75", "babel-core": "^6.26.0", "babel-eslint": "^8.2.6", "babel-jest": "^23.4.2", "babel-loader": "^7.1.0", "babel-plugin-transform-class-properties": "^6.23.0", "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-react-jsx": "^6.23.0", "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.24.1", "babili": "^0.1.4", "babili-webpack-plugin": "^0.1.2", "bestzip": "^2.1.2", "copy-webpack-plugin": "^4.0.1", "css-loader": "^1.0.0", "eslint": "^5.4.0", "eslint-config-airbnb": "^17.1.0", "eslint-config-prettier": "^3.0.1", "eslint-plugin-import": "^2.10.0", "eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-node": "^8.0.0", "eslint-plugin-prettier": "^3.0.0", "eslint-plugin-promise": "^4.0.0", "eslint-plugin-react": "^7.2.0", "eslint-plugin-standard": "^4.0.0", "extract-loader": "^3.0.0", "generate-json-webpack-plugin": "^0.3.1", "html-loader": "^0.5.5", "jest": "^23.5.0", "jest-dom": "^2.1.1", "markdown-loader": "^4.0.0", "node-sass": "^4.8.3", "preact-render-spy": "^1.2.2", "preact-render-to-string": "^4.1.0", "preact-test-utils": "^0.1.3", "preact-testing-library": "^0.3.0", "prettier": "^1.14.2", "rimraf": "^2.6.2", "sass-loader": "^7.1.0", "sinon-chrome": "^2.3.1", "style-loader": "^0.23.1", "webpack": "^4.17.0", "webpack-cli": "^3.1.0", "webpack-merge": "^4.1.4" }, "dependencies": { "fuse.js": "^3.2.0", "marked": "^0.6.2", "material-components-web": "^0.38.2", "msgx": "^1.1.3", "preact": "^8.3.0", "preact-compat": "^3.18.0", "webextension-polyfill": "^0.4.0" } } ================================================ FILE: spec/support/jasmine.json ================================================ { "spec_dir": "spec", "spec_files": [ "**/*[sS]pec.js" ], "helpers": [ "helpers/**/*.js" ], "stopSpecOnExpectationFailure": false, "random": false } ================================================ FILE: src/background_page/index.js ================================================ import browser from 'webextension-polyfill'; import 'msg/server.js'; import { tabHistory, recentlyClosed } from './tabHistory.js'; window.tabHistory = tabHistory; window.recentlyClosed = recentlyClosed; let lastTabId; async function toggleSaka(tabId) { if (SAKA_DEBUG) console.group('toggleSaka'); // Get the specified tab, or the current tab if none is specified const currentTab = tabId === undefined ? (await browser.tabs.query({ active: true, currentWindow: true }))[0] : await browser.tabs.get(tabId); if (currentTab) { // If the current tab is Saka, switch to the previous tab (if it exists) and close the current tab if (currentTab.url === browser.runtime.getURL('saka.html')) { if (lastTabId) { try { const lastTab = await browser.tabs.get(lastTabId); if (lastTab) { try { await browser.tabs.update(lastTabId, { active: true }); if (SAKA_DEBUG) console.log(`Switched to tab ${lastTab.url}`); } catch (e) { if (SAKA_DEBUG) console.error(`Failed to switch to tab ${lastTab.url}`); } } lastTabId = undefined; } catch (e) { if (SAKA_DEBUG) console.error( `Cannot return to tab ${lastTabId} because it no longer exists` ); } } try { await browser.tabs.remove(currentTab.id); if (SAKA_DEBUG) console.log(`Removed tab ${currentTab.url}`); } catch (e) { if (SAKA_DEBUG) console.error(`Failed to remove tab ${currentTab.url}`); } // Otherwise, try to load Saka into the current tab } else { try { await browser.tabs.executeScript(currentTab.id, { file: '/toggle_saka.js', runAt: 'document_start', matchAboutBlank: true }); if (SAKA_DEBUG) console.log(`Loaded Saka into tab ${currentTab.url}`); // If loading Saka into the current tab fails, create a new tab } catch (e) { try { const screenshot = await browser.tabs.captureVisibleTab(); await browser.storage.local.set({ screenshot }); } catch (screenshotError) { if (SAKA_DEBUG) console.error('Failed to capture visible tab: ', screenshotError); } lastTabId = currentTab.id; await browser.tabs.create({ url: '/saka.html', index: currentTab.index, active: false }); if (SAKA_DEBUG) console.warn( `Failed to execute Saka into tab. Instead, created new Saka tab after ${ currentTab.url }` ); } } // If tab couldn't be found (e.g. because query was made from devtools) create a new tab } else { await browser.tabs.create({ url: '/saka.html' }); if (SAKA_DEBUG) console.log("Couldn't find tab. Instead, created new Saka tab."); } const window = await browser.windows.getLastFocused(); await browser.windows.update(window.id, { focused: true }); if (SAKA_DEBUG) console.groupEnd(); } async function closeSaka(tab) { if (tab) { if (tab.url === browser.runtime.getURL('saka.html')) { await browser.tabs.remove(tab.id); } else { await browser.tabs.executeScript(tab.id, { file: '/toggle_saka.js', runAt: 'document_start', matchAboutBlank: true }); } } } async function saveSettings(searchHistory) { await browser.storage.sync.set({ searchHistory: [...searchHistory] }); } browser.browserAction.onClicked.addListener(() => { toggleSaka(); }); browser.commands.onCommand.addListener(command => { switch (command) { case 'toggleSaka': case 'toggleSaka2': case 'toggleSaka3': case 'toggleSaka4': toggleSaka(); break; default: console.error(`Unknown command: '${command}'`); } }); browser.runtime.onMessage.addListener(async (message, sender) => { switch (message.key) { case 'toggleSaka': toggleSaka(); break; case 'closeSaka': await saveSettings(message.searchHistory); closeSaka(sender.tab); break; default: console.error(`Unknown message: '${message}'`); } }); browser.runtime.onMessageExternal.addListener(message => { switch (message) { case 'toggleSaka': toggleSaka(); break; default: console.error(`Unknown message: '${message}'`); } }); browser.contextMenus.create({ title: 'Saka', contexts: ['all'], onclick: () => toggleSaka() }); ================================================ FILE: src/background_page/tabHistory.js ================================================ import browser from 'webextension-polyfill'; // list of tab ids in order of increasing age since last visit export const tabHistory = []; // list of tab ids in order of increasing age since closed export const recentlyClosed = []; const log = listener => (...args) => { listener(...args); // if (SAKA_DEBUG) console.log(tabHistory); }; function setMostRecentTab(tabInfo) { const tabIndex = tabHistory.findIndex(tab => tab.tabId === tabInfo.tabId); if (tabIndex !== -1) { tabHistory.splice(tabIndex, 1); } tabHistory.unshift(tabInfo); } function setMostRecentClosedTab(tabInfo) { const tabIndex = recentlyClosed.findIndex(tab => tab.tabId === tabInfo.tabId); if (tabIndex !== -1) { recentlyClosed.splice(tabIndex, 1); } recentlyClosed.unshift(tabInfo); } browser.tabs.onActivated.addListener( log(({ tabId }) => { setMostRecentTab({ tabId, lastAccessed: Date.now() }); }) ); browser.tabs.onRemoved.addListener( log(tabId => { const i = tabHistory.findIndex(tab => tab.tabId === tabId); tabHistory.splice(i, 1); setMostRecentClosedTab({ tabId, lastAccessed: Date.now() }); }) ); browser.tabs.onReplaced.addListener( log((addedTabId, removedTabId) => { const i = tabHistory.findIndex(tab => tab.tabId === removedTabId); tabHistory[i] = { tabId: addedTabId, lastAccessed: Date.now() }; }) ); browser.windows.onFocusChanged.addListener(async windowId => { const [tab] = await browser.tabs.query({ currentWindow: true, active: true }); if (tab && tab.windowId === windowId) { setMostRecentTab({ tabId: tab.id, lastAccessed: Date.now() }); } }); ================================================ FILE: src/content_script/toggle_saka.js ================================================ // this file is dynamically loaded by the event page into the active tab of the active window // Search for an existing Saka // * if found, remove it // * if not found, create and show it const oldSakaRoot = document.querySelector('#saka-root'); if (oldSakaRoot) { if (SAKA_DEBUG) console.log('REMOVING SAKA'); oldSakaRoot.remove(); } else { if (SAKA_DEBUG) console.log('APPENDING SAKA'); // create container div const newSakaRoot = document.createElement('div'); newSakaRoot.id = 'saka-root'; newSakaRoot.style = `position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: 2147483647; opacity: 1; pointer-events: none;`; // create Saka iframe const iframe = document.createElement('iframe'); iframe.id = 'saka'; iframe.src = chrome.runtime.getURL('saka.html'); iframe.style = `z-index: 2147483647; position: fixed; left: 0; top: 0; width: 100%; height: 100%; border-width: 0; pointer-events: all; }`; iframe.frameBorder = 0; // mount to DOM newSakaRoot.appendChild(iframe); document.documentElement.appendChild(newSakaRoot); } ================================================ FILE: src/lib/colors.js ================================================ export const colors = { red: 'rgba(228,26,28,1)', black: 'rgba(0,0,0,1)', blue: 'rgba(55,126,184,1)', green: 'rgba(77,175,74,1)', purple: 'rgba(152,78,163,1)', orange: 'rgba(255,127,0,1)', yellow: 'rgba(255,255,51,1)', brown: 'rgba(166,86,40,1)', pink: 'rgba(247,129,191,1)', teal: 'rgb(0,77,64,1)', gray: 'rgba(153,153,153,1)' }; export const fadedColors = { red: 'rgba(228,26,28,0.44)', black: 'rgba(0,0,0,0.44)', blue: 'rgba(55,126,184,0.44)', green: 'rgba(77,175,74,0.44)', purple: 'rgba(152,78,163,0.44)', orange: 'rgba(255,127,0,0.44)', yellow: 'rgba(255,255,51,0.44)', brown: 'rgba(166,86,40,0.44)', pink: 'rgba(247,129,191,0.44)', teal: 'rgb(0,77,64,0.44)', gray: 'rgba(153,153,153,0.44)' }; export const colorMap = { tab: colors.blue, closedTab: colors.black, search: colors.green, history: colors.red, recentlyViewed: colors.purple, bookmark: colors.orange, mode: colors.purple, // dictionary: colors.purple, calculator: colors.brown, // command: colors.pink, unknown: colors.gray }; export const fadedColorMap = { tab: fadedColors.blue, closedTab: fadedColors.black, search: fadedColors.green, history: fadedColors.red, recentlyViewed: fadedColors.purple, bookmark: fadedColors.orange, mode: fadedColors.purple, // dictionary: colors.purple, calculator: fadedColors.brown, // command: fadedColors.pink, unknown: fadedColors.gray }; ================================================ FILE: src/lib/dom.js ================================================ export function slowWheelEvent( threshold, onPositiveThreshold, onNegativeThreshold, value = 0 ) { return e => { e.preventDefault(); let val = value; val += e.deltaY; if (val >= threshold) { val = 0; onPositiveThreshold(e); } else if (val <= -threshold) { val = 0; onNegativeThreshold(e); } }; } /** * Return whether the cursor is at the far right end of the * provided HTML input * @param {HTMLInputElement} input */ export function cursorAtEnd(input) { return input && input.selectionStart === input.value.length; } ================================================ FILE: src/lib/highlight.jsx ================================================ import { h } from 'preact'; function highlighted(text, indices) { const out = []; let unit = ''; let pairIndex = 0; let pair = indices[pairIndex]; pairIndex += 1; for (let i = 0; i < text.length; i++) { const char = text[i]; if (pair && i === pair[0]) { out.push(unit); unit = ''; } unit += char; if (pair && i === pair[1]) { out.push({unit}); unit = ''; pair = indices[pairIndex]; pairIndex += 1; } } if (unit !== '') out.push(unit); return out; } export default function highlight(text, key, matches) { const matchesForKey = matches && matches.find(match => match.key === key); return matchesForKey ? highlighted(text, matchesForKey.indices) : text; } ================================================ FILE: src/lib/log.js ================================================ export default (thing, ...things) => { if (SAKA_DEBUG) { console.log(thing, ...things); } return thing; }; ================================================ FILE: src/lib/tld.js ================================================ export default [ 'abogado', 'ac', 'academy', 'accountants', 'active', 'actor', 'ad', 'adult', 'ae', 'aero', 'af', 'ag', 'agency', 'ai', 'airforce', 'al', 'allfinanz', 'alsace', 'am', 'amsterdam', 'an', 'android', 'ao', 'aq', 'aquarelle', 'ar', 'archi', 'army', 'arpa', 'as', 'asia', 'associates', 'at', 'attorney', 'au', 'auction', 'audio', 'autos', 'aw', 'ax', 'axa', 'az', 'ba', 'band', 'bank', 'bar', 'barclaycard', 'barclays', 'bargains', 'bayern', 'bb', 'bd', 'be', 'beer', 'berlin', 'best', 'bf', 'bg', 'bh', 'bi', 'bid', 'bike', 'bio', 'biz', 'bj', 'black', 'blackfriday', 'bloomberg', 'blue', 'bm', 'bmw', 'bn', 'bnpparibas', 'bo', 'boo', 'boutique', 'br', 'brussels', 'bs', 'bt', 'budapest', 'build', 'builders', 'business', 'buzz', 'bv', 'bw', 'by', 'bz', 'bzh', 'ca', 'cab', 'cal', 'camera', 'camp', 'cancerresearch', 'capetown', 'capital', 'caravan', 'cards', 'care', 'career', 'careers', 'cartier', 'casa', 'cash', 'cat', 'catering', 'cc', 'cd', 'center', 'ceo', 'cern', 'cf', 'cg', 'ch', 'channel', 'cheap', 'christmas', 'chrome', 'church', 'ci', 'citic', 'city', 'ck', 'cl', 'claims', 'cleaning', 'click', 'clinic', 'clothing', 'club', 'cm', 'cn', 'co', 'coach', 'codes', 'coffee', 'college', 'cologne', 'com', 'community', 'company', 'computer', 'condos', 'construction', 'consulting', 'contractors', 'cooking', 'cool', 'coop', 'country', 'cr', 'credit', 'creditcard', 'cricket', 'crs', 'cruises', 'cu', 'cuisinella', 'cv', 'cw', 'cx', 'cy', 'cymru', 'cz', 'dabur', 'dad', 'dance', 'dating', 'day', 'dclk', 'de', 'deals', 'degree', 'delivery', 'democrat', 'dental', 'dentist', 'desi', 'design', 'dev', 'diamonds', 'diet', 'digital', 'direct', 'directory', 'discount', 'dj', 'dk', 'dm', 'dnp', 'do', 'docs', 'domains', 'doosan', 'durban', 'dvag', 'dz', 'eat', 'ec', 'edu', 'education', 'ee', 'eg', 'email', 'emerck', 'energy', 'engineer', 'engineering', 'enterprises', 'equipment', 'er', 'es', 'esq', 'estate', 'et', 'eu', 'eurovision', 'eus', 'events', 'everbank', 'exchange', 'expert', 'exposed', 'fail', 'farm', 'fashion', 'feedback', 'fi', 'finance', 'financial', 'firmdale', 'fish', 'fishing', 'fit', 'fitness', 'fj', 'fk', 'flights', 'florist', 'flowers', 'flsmidth', 'fly', 'fm', 'fo', 'foo', 'forsale', 'foundation', 'fr', 'frl', 'frogans', 'fund', 'furniture', 'futbol', 'ga', 'gal', 'gallery', 'garden', 'gb', 'gbiz', 'gd', 'ge', 'gent', 'gf', 'gg', 'ggee', 'gh', 'gi', 'gift', 'gifts', 'gives', 'gl', 'glass', 'gle', 'global', 'globo', 'gm', 'gmail', 'gmo', 'gmx', 'gn', 'goog', 'google', 'gop', 'gov', 'gp', 'gq', 'gr', 'graphics', 'gratis', 'green', 'gripe', 'gs', 'gt', 'gu', 'guide', 'guitars', 'guru', 'gw', 'gy', 'hamburg', 'hangout', 'haus', 'healthcare', 'help', 'here', 'hermes', 'hiphop', 'hiv', 'hk', 'hm', 'hn', 'holdings', 'holiday', 'homes', 'horse', 'host', 'hosting', 'house', 'how', 'hr', 'ht', 'hu', 'ibm', 'id', 'ie', 'ifm', 'il', 'im', 'immo', 'immobilien', 'in', 'industries', 'info', 'ing', 'ink', 'institute', 'insure', 'int', 'international', 'investments', 'io', 'iq', 'ir', 'irish', 'is', 'it', 'iwc', 'jcb', 'je', 'jetzt', 'jm', 'jo', 'jobs', 'joburg', 'jp', 'juegos', 'kaufen', 'kddi', 'ke', 'kg', 'kh', 'ki', 'kim', 'kitchen', 'kiwi', 'km', 'kn', 'koeln', 'kp', 'kr', 'krd', 'kred', 'kw', 'ky', 'kyoto', 'kz', 'la', 'lacaixa', 'land', 'lat', 'latrobe', 'lawyer', 'lb', 'lc', 'lds', 'lease', 'legal', 'lgbt', 'li', 'lidl', 'life', 'lighting', 'limited', 'limo', 'link', 'lk', 'loans', 'london', 'lotte', 'lotto', 'lr', 'ls', 'lt', 'ltda', 'lu', 'luxe', 'luxury', 'lv', 'ly', 'ma', 'madrid', 'maison', 'management', 'mango', 'market', 'marketing', 'marriott', 'mc', 'md', 'me', 'media', 'meet', 'melbourne', 'meme', 'memorial', 'menu', 'mg', 'mh', 'miami', 'mil', 'mini', 'mk', 'ml', 'mm', 'mn', 'mo', 'mobi', 'moda', 'moe', 'monash', 'money', 'mormon', 'mortgage', 'moscow', 'motorcycles', 'mov', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nagoya', 'name', 'navy', 'nc', 'ne', 'net', 'network', 'neustar', 'new', 'nexus', 'nf', 'ng', 'ngo', 'nhk', 'ni', 'ninja', 'nl', 'no', 'np', 'nr', 'nra', 'nrw', 'nu', 'nyc', 'nz', 'okinawa', 'om', 'one', 'ong', 'onl', 'ooo', 'org', 'organic', 'osaka', 'otsuka', 'ovh', 'pa', 'paris', 'partners', 'parts', 'party', 'pe', 'pf', 'pg', 'ph', 'pharmacy', 'photo', 'photography', 'photos', 'physio', 'pics', 'pictures', 'pink', 'pizza', 'pk', 'pl', 'place', 'plumbing', 'pm', 'pn', 'pohl', 'poker', 'porn', 'post', 'pr', 'praxi', 'press', 'pro', 'prod', 'productions', 'prof', 'properties', 'property', 'ps', 'pt', 'pub', 'pw', 'py', 'qa', 'qpon', 'quebec', 're', 'realtor', 'recipes', 'red', 'rehab', 'reise', 'reisen', 'reit', 'ren', 'rentals', 'repair', 'report', 'republican', 'rest', 'restaurant', 'reviews', 'rich', 'rio', 'rip', 'ro', 'rocks', 'rodeo', 'rs', 'rsvp', 'ru', 'ruhr', 'rw', 'ryukyu', 'sa', 'saarland', 'sale', 'samsung', 'sarl', 'sb', 'sc', 'sca', 'scb', 'schmidt', 'schule', 'schwarz', 'science', 'scot', 'sd', 'se', 'services', 'sew', 'sexy', 'sg', 'sh', 'shiksha', 'shoes', 'shriram', 'si', 'singles', 'sj', 'sk', 'sky', 'sl', 'sm', 'sn', 'so', 'social', 'software', 'sohu', 'solar', 'solutions', 'soy', 'space', 'spiegel', 'sr', 'st', 'su', 'supplies', 'supply', 'support', 'surf', 'surgery', 'suzuki', 'sv', 'sx', 'sy', 'sydney', 'systems', 'sz', 'taipei', 'tatar', 'tattoo', 'tax', 'tc', 'td', 'technology', 'tel', 'temasek', 'tf', 'tg', 'th', 'tienda', 'tips', 'tires', 'tirol', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'today', 'tokyo', 'tools', 'top', 'town', 'toys', 'tp', 'tr', 'trade', 'training', 'travel', 'trust', 'tt', 'tui', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk', 'university', 'uno', 'uol', 'us', 'uy', 'uz', 'va', 'vacations', 'vc', 've', 'vegas', 'ventures', 'versicherung', 'vet', 'vg', 'vi', 'viajes', 'video', 'villas', 'vision', 'vlaanderen', 'vn', 'vodka', 'vote', 'voting', 'voto', 'voyage', 'vu', 'wales', 'wang', 'watch', 'webcam', 'website', 'wed', 'wedding', 'wf', 'whoswho', 'wien', 'wiki', 'williamhill', 'wme', 'work', 'works', 'world', 'ws', 'wtc', 'wtf', 'xn--1qqw23a', 'xn--3bst00m', 'xn--3ds443g', 'xn--3e0b707e', 'xn--45brj9c', 'xn--45q11c', 'xn--4gbrim', 'xn--55qw42g', 'xn--55qx5d', 'xn--6frz82g', 'xn--6qq986b3xl', 'xn--80adxhks', 'xn--80ao21a', 'xn--80asehdb', 'xn--80aswg', 'xn--90a3ac', 'xn--b4w605ferd', 'xn--c1avg', 'xn--cg4bki', 'xn--clchc0ea0b2g2a9gcd', 'xn--czr694b', 'xn--czrs0t', 'xn--czru2d', 'xn--d1acj3b', 'xn--d1alf', 'xn--fiq228c5hs', 'xn--fiq64b', 'xn--fiqs8s', 'xn--fiqz9s', 'xn--flw351e', 'xn--fpcrj9c3d', 'xn--fzc2c9e2c', 'xn--gecrj9c', 'xn--h2brj9c', 'xn--hxt814e', 'xn--i1b6b1a6a2e', 'xn--io0a7i', 'xn--j1amh', 'xn--j6w193g', 'xn--kprw13d', 'xn--kpry57d', 'xn--kput3i', 'xn--l1acc', 'xn--lgbbat1ad8j', 'xn--mgb9awbf', 'xn--mgba3a4f16a', 'xn--mgbaam7a8h', 'xn--mgbab2bd', 'xn--mgbayh7gpa', 'xn--mgbbh1a71e', 'xn--mgbc0a9azcg', 'xn--mgberp4a5d4ar', 'xn--mgbx4cd0ab', 'xn--ngbc5azd', 'xn--node', 'xn--nqv7f', 'xn--nqv7fs00ema', 'xn--o3cw4h', 'xn--ogbpf8fl', 'xn--p1acf', 'xn--p1ai', 'xn--pgbs0dh', 'xn--q9jyb4c', 'xn--qcka1pmc', 'xn--rhqv96g', 'xn--s9brj9c', 'xn--ses554g', 'xn--unup4y', 'xn--vermgensberater-ctb', 'xn--vermgensberatung-pwb', 'xn--vhquv', 'xn--wgbh1c', 'xn--wgbl6a', 'xn--xhq521b', 'xn--xkc2al3hye2a', 'xn--xkc2dl3a5ee0h', 'xn--yfro4i67o', 'xn--ygbi2ammx', 'xn--zfr164b', 'xxx', 'xyz', 'yachts', 'yandex', 'ye', 'yoga', 'yokohama', 'youtube', 'yt', 'za', 'zip', 'zm', 'zone', 'zuerich', 'zw' ]; ================================================ FILE: src/lib/trie.js ================================================ /** * A Trie datastructure that uses a simple javascript object for its underlying storage. * It doesn't generate the input tree for you, you MUST generate it yourself. * This trie is designed for Saka Key's needs and ISN'T general purpose. * An example input tree is: * { * "c": { * "a": { * "t": "Tom", * "r": "Tarzan" * } * }, * "d": { * "o": { * "g": () => console.log("My dog's name is Harris") * } * } * } */ export default class Trie { /** * Creates a trie * @param {Object} root - A simple javascript object representing a trie */ init = root => { this.root = root; this.curNode = root; }; /** Sets the root to current node to the root node */ reset = () => { this.curNode = this.root; }; /** * Advances the command trie based on the command key event. * If a leaf node, corresponding to a command, has been reached, * returns the command. * Otherwise returns undefined */ advance = input => { // TODO: Update to use longest viable prefix by trying // longest prefix until a valid path is found const next = this.curNode[input] || this.root[input] || this.root; // Case 1. A trie node if (typeof next === 'object') { this.curNode = next; return undefined; // Case 2. A trie leaf corresponding to the command reached } this.curNode = this.root; return next; }; } ================================================ FILE: src/lib/url.js ================================================ import browser from 'webextension-polyfill'; import knownTLDs from './tld.js'; /** * Given the URL of a suggestion and the search text, makes the URL nicer * @param {string} url - the suggestion URL * @param {string} searchString - the text in the search bar */ export function prettifyURL(url, searchString) { let prettifiedUrl = url; if (url.endsWith('/')) { prettifiedUrl = url.substr(0, url.length - 1); } // TODO add support for any protocol if ( !searchString.startsWith('http://') && prettifiedUrl.startsWith('http://') ) { prettifiedUrl = prettifiedUrl.substr(7); } return prettifiedUrl; } /** * Returns true only if str is a valid url * @param {string} str */ export function isURL(str) { let isValidUrl; try { isValidUrl = Boolean(new URL(str)); } catch (e) { isValidUrl = false; } return isValidUrl; } export function extractProtocol(url) { if (url) { return url.match(/^\w+:/, '') ? url.match(/^\w+:/, '')[0] : ''; } return ''; } export function stripProtocol(url) { return url.replace(/(^\w+:|^)\/\//, ''); } export function stripWWW(url) { return url.replace(/^www\./, ''); } export function startsWithProtocol(str) { return str.match(/(^\w+:|^)\/\//) !== null; } export function startsWithWWW(str) { return str.match(/^www\./, '') !== null; } /** Returns whether the provided text is a known TLD (top-level domain) */ export function isTLD(text) { return knownTLDs.indexOf(text) !== -1; } const knownProtocols = [ 'http:', 'https:', 'file:', 'ftp:', 'about:', 'chrome:', 'chrome-extension:', 'moz-extension:' ]; /** Returns whether the provided text is a known protocol */ export function isProtocol(text) { return knownProtocols.indexOf(text) !== -1; } export function isLikeURL(url) { let trimmedUrl = url.trim(); if (trimmedUrl.indexOf(' ') !== -1) { return false; } if (trimmedUrl.search(/^(about|file):[^:]/) !== -1) { return true; } const protocol = (trimmedUrl.match(/^([a-zA-Z-]+:)[^:]/) || [''])[0].slice( 0, -1 ); const protocolMatch = isProtocol(protocol); if (protocolMatch) { trimmedUrl = trimmedUrl.replace(/^[a-zA-Z-]+:\/*/, ''); } const hasPath = /.*[a-zA-Z].*\//.test(trimmedUrl); trimmedUrl = trimmedUrl.replace(/(:[0-9]+)?([#/].*|$)/g, '').split('.'); if (protocolMatch && /^[a-zA-Z0-9@!]+$/.test(trimmedUrl)) { return true; } if (protocol && !protocolMatch && protocol !== 'localhost:') { return false; } // IP addresses const isIP = trimmedUrl.every(e => /^[0-9]+$/.test(e) && +e >= 0 && +e < 256); if ( (isIP && !protocol && trimmedUrl.length === 4) || (isIP && protocolMatch) ) { return true; } return ( (trimmedUrl.every(e => /^[a-z0-9-]+$/i.test(e)) && (trimmedUrl.length > 1 && isTLD(trimmedUrl[trimmedUrl.length - 1]))) || (trimmedUrl.length === 1 && trimmedUrl[0] === 'localhost') || hasPath ); } export async function isSakaUrl(url) { if (url !== undefined) { const sakaUrl = browser.runtime.getURL('saka.html'); const sakaId = sakaUrl.substring(0, sakaUrl.indexOf('/')); return url.includes(sakaId); } return false; } ================================================ FILE: src/lib/utils.js ================================================ import Fuse from 'fuse.js'; export const isMac = navigator.appVersion.indexOf('Mac') !== -1; export const ctrlChar = isMac ? '⌘' : 'ctrl'; export function rangedIncrement(value, increment, min, max) { const result = value + increment; if (result < min) { return min; } else if (result > max) { return max; } return result; } /** * @param {KeyboardEvent} e */ export function ctrlKey(e) { return isMac ? e.metaKey : e.ctrlKey; } export function objectFromArray(array, key) { const out = {}; array.forEach(e => { out[e[key]] = e; }); return out; } export async function getFilteredSuggestions( searchString, { getSuggestions, threshold, keys } ) { const suggestions = await getSuggestions(searchString); const fuse = new Fuse(suggestions, { shouldSort: true, threshold, minMatchCharLength: 1, includeMatches: true, keys, distance: 500 }); return fuse.search(searchString).map(({ item, matches, score }) => ({ ...item, score, matches })); } ================================================ FILE: src/msg/client.js ================================================ import client from 'msgx/client.js'; const msg = client({ zoom: zoom => { window.dispatchEvent(new CustomEvent('zoom', { detail: { zoom } })); } }); export default msg; ================================================ FILE: src/msg/server.js ================================================ import browser from 'webextension-polyfill'; import server from 'msgx/server.js'; import { getSuggestions, activateSuggestion, closeTab } from 'suggestion_engine/server/index.js'; const actions = { // endpoints client queries with msg() sg: getSuggestions, zoom: (_, sender) => browser.tabs.getZoom(sender.tab.id), focusTab: (_, sender) => browser.tabs.update(sender.tab.id, { active: true }), activateSuggestion, closeTab }; const onConnect = (sender, msg, data) => { const onZoomChange = ({ tabId, newZoomFactor }) => { if (sender.tab.id === tabId) { msg('zoom', newZoomFactor); } }; browser.tabs.onZoomChange.addListener(onZoomChange); data.onZoomChange = onZoomChange; }; const onDisconnect = (sender, data) => { browser.tabs.onZoomChange.removeListener(data.onZoomChange); }; server(actions, onConnect, onDisconnect); ================================================ FILE: src/options/Main/MainOptions.jsx ================================================ import { Component, h } from 'preact'; import 'material-components-web/dist/material-components-web.css'; import 'scss/options.scss'; import OptionsList from './OptionsList/index.jsx'; import SakaHotkeysList from './SakaHotkeysList/index.jsx'; export default class MainOptions extends Component { constructor(props) { super(props); this.state = { showSakaKeybindings: false }; } handleOpenSakaKeybindings = () => { this.setState({ showSakaKeybindings: !this.state.showSakaKeybindings }); }; render() { return (
Saka Options
{this.state.showSakaKeybindings ? ( ) : ( )}
); } } ================================================ FILE: src/options/Main/OptionsList/DefaultModeSelection.jsx ================================================ import { h } from 'preact'; // import { Component, h } from 'preact'; import 'material-components-web/dist/material-components-web.css'; const DefaultModeSelection = function DefaultModeSelection({ mode, handleModeChange }) { return (
  • Default Mode Select the default mode Saka opens with
  • ); }; export default DefaultModeSelection; ================================================ FILE: src/options/Main/OptionsList/EnableFuzzySearch.jsx ================================================ import { h } from 'preact'; const EnableFuzzySearch = function EnableFuzzySearch() { const { checked, handleEnableFuzzySearch } = this.props; return (
  • Enable fuzzy search Enable fuzzy search for bookmarks and history search
  • ); }; export default EnableFuzzySearch; ================================================ FILE: src/options/Main/OptionsList/OnlyShowSearchBarSelector.jsx ================================================ import { h } from 'preact'; const OnlyShowSearchBarSelector = function OnlyShowSearchBarSelector() { const { checked, handleShowSearchSuggestionsChange } = this.props; return (
  • Suggestions on load Show suggestions when there is no text is the Saka search bar
  • ); }; export default OnlyShowSearchBarSelector; ================================================ FILE: src/options/Main/OptionsList/ShowSakaHotkeys.jsx ================================================ import { h } from 'preact'; import 'material-components-web/dist/material-components-web.css'; const ShowSakaHotkeys = function ShowSakaHotkeys({ handleOpenSakaKeybindings }) { return (
  • Saka Hotkeys keyboard
  • ); }; export default ShowSakaHotkeys; ================================================ FILE: src/options/Main/OptionsList/index.jsx ================================================ import browser from 'webextension-polyfill'; import { Component, h } from 'preact'; import DefaultModeSelection from './DefaultModeSelection.jsx'; import OnlyShowSearchBarSelector from './OnlyShowSearchBarSelector.jsx'; import ShowSakaHotkeys from './ShowSakaHotkeys.jsx'; import EnableFuzzySearch from './EnableFuzzySearch.jsx'; export default class OptionsList extends Component { constructor(props) { super(props); this.state = { isLoading: true, mode: 'tab', showEmptySearchSuggestions: true, enableFuzzySearch: true }; } async componentDidMount() { const sakaSettings = await this.fetchSakaSettings(); this.setState(sakaSettings); } fetchSakaSettings = async function fetchSakaSettings() { const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']); if (sakaSettings !== undefined) { return { isLoading: false, mode: sakaSettings.mode, showEmptySearchSuggestions: sakaSettings.showEmptySearchSuggestions, enableFuzzySearch: sakaSettings.enableFuzzySearch }; } return { isLoading: false }; }; handleOptionsSave = () => { const settingsStore = { mode: this.state.mode, showEmptySearchSuggestions: this.state.showEmptySearchSuggestions, enableFuzzySearch: this.state.enableFuzzySearch }; browser.storage.sync.set({ sakaSettings: settingsStore }); }; handleModeChange = e => { this.setState({ mode: e.target.value }); }; handleShowSearchSuggestionsChange = () => { this.setState({ showEmptySearchSuggestions: !this.state.showEmptySearchSuggestions }); }; handleEnableFuzzySearch = () => { this.setState({ enableFuzzySearch: !this.state.enableFuzzySearch }); }; render() { const { handleOpenSakaKeybindings } = this.props; if (!this.state.isLoading) { return (

    General Settings

    ); } return
    ; } } ================================================ FILE: src/options/Main/SakaHotkeysList/HotkeyListRow.jsx ================================================ import { h } from 'preact'; import 'material-components-web/dist/material-components-web.css'; const HotkeyListRow = function HotkeyListRow({ title, keys }) { const hotkeyShortcut = keys.map((key, index, keysArray) => ( {key} {keysArray.length === index + 1 ? '' : '+'} )); return (
  • {title}
    {hotkeyShortcut}
  • ); }; export default HotkeyListRow; ================================================ FILE: src/options/Main/SakaHotkeysList/index.jsx ================================================ import { h } from 'preact'; import HotkeyListRow from './HotkeyListRow.jsx'; import 'material-components-web/dist/material-components-web.css'; import { ctrlChar } from 'lib/utils'; const SakaHotkeysList = function SakaHotkeysList({ handleOpenSakaKeybindings }) { return (
    arrow_back
    info {SAKA_PLATFORM === 'chrome' ? ( To modify the Saka hotkeys, please visit chrome://extensions/shortcuts ) : ( It is currently not possible to modify hotkeys in firefox )}

    Keyboard Shortcuts

    ); }; export default SakaHotkeysList; ================================================ FILE: src/options/saka-options.jsx ================================================ import { render, h } from 'preact'; import Main from './Main/MainOptions.jsx'; render(
    , document.body); ================================================ FILE: src/saka/Main/Components/BackgroundImage/index.jsx ================================================ import browser from 'webextension-polyfill'; import { h, Component } from 'preact'; import msg from 'msg/client.js'; import 'scss/styles.scss'; export default class BackgroundImage extends Component { state = { screenshot: undefined }; componentDidMount() { (async () => { const { screenshot } = await browser.storage.local.get('screenshot'); this.setState({ screenshot }); await msg('focusTab'); await browser.storage.local.remove('screenshot'); })(); } render() { const { children } = this.props; const { screenshot } = this.state; return (
    {children}
    ); } // componentWillReceiveProps (nextProps) { // if (nextProps.suggestion.tabId !== this.props.suggestion.tabId) { // this.fetchImage(nextProps.suggestion.tabId); // } // } // shouldComponentUpdate (nextProps, nextState) { // return nextState.image !== this.state.image; // } // fetchImage = async () => { // } } ================================================ FILE: src/saka/Main/Components/GUIContainer/index.jsx ================================================ import { h, Component } from 'preact'; import msg from 'msg/client.js'; import 'scss/styles.scss'; // Makes GUI constant size export default class GUIContainer extends Component { state = { zoom: 0 }; componentWillMount() { window.addEventListener('zoom', this.onZoomChange); msg('zoom').then(this.setZoom); } componentWillUnmount() { window.removeEventListener('zoom', this.onZoomChange); } onZoomChange = event => { this.setZoom(event.detail.zoom); }; setZoom = zoom => { this.setState({ zoom }); }; render() { const { children, onWheel } = this.props; const { zoom } = this.state; // opacity: 0.01 is just a trick to hide the component and not prevent it from // from rendering/mounting in the DOM, which would preven the search bar from focusing return (
    {children}
    ); } } ================================================ FILE: src/saka/Main/Components/Icon/index.jsx ================================================ import { h } from 'preact'; import 'scss/styles.scss'; export default ({ icon, color }) => { return ( ); }; ================================================ FILE: src/saka/Main/Components/ModeSwitcher/index.jsx ================================================ import { h } from 'preact'; import { suggestions } from 'src/suggestion_engine/server/providers/mode.js'; import Icon from 'src/saka/Main/Components/Icon/index.jsx'; import { fadedColorMap } from 'lib/colors.js'; import 'scss/styles.scss'; export default ({ mode, setMode }) => { const validModes = suggestions.map(suggestion => { const color = suggestion.mode === mode ? suggestion.fadedColor : fadedColorMap.unknown; return (
    setMode(suggestion.mode)} >
    ); }); return
    {validModes}
    ; }; ================================================ FILE: src/saka/Main/Components/PaginationBar/index.jsx ================================================ import { h } from 'preact'; import { ctrlChar } from 'lib/utils.js'; import 'scss/styles.scss'; export default ({ firstVisibleIndex, suggestions, maxSuggestions, onClickPrevious, onClickNext }) => suggestions.length === 0 ? ( undefined ) : (
    {ctrlChar}-S
    {`${firstVisibleIndex + 1} - ${firstVisibleIndex + Math.min(suggestions.length, maxSuggestions)} / ${ suggestions.length }`}
    {ctrlChar}-D ►
    ); ================================================ FILE: src/saka/Main/Components/SearchBar/Button/index.jsx ================================================ import { h, Component } from 'preact'; import '@material/button/dist/mdc.button.min.css'; import { icons } from 'suggestion_utils/index.js'; import { colorMap, fadedColorMap } from 'lib/colors.js'; import 'scss/styles.scss'; // 1. Reload // 2. Search // 3. History // 4. Calculate // 5. Activate // 6. Go // 7. Command // function icon (searchText, searchValue, tabURL, modifiers) { // return searchText === tabURL // ? 'refresh' // : iconForType[isURL(searchValue) ? 'url' : 'search']; // } // function icon (suggestion) { // if (suggestion) { // switch (suggestion.type) { // case 'tab': // return 'tab'; // case 'closedTab': // return 'restore'; // } // } // return 'error'; // } export default class extends Component { state = { hovered: false }; handleMouseEnter = () => { this.setState({ hovered: true }); }; handleMouseLeave = () => { this.setState({ hovered: false }); }; render() { const { mode, onClick } = this.props; const { hovered } = this.state; const { handleMouseEnter, handleMouseLeave } = this; return (
    ); } } ================================================ FILE: src/saka/Main/Components/SearchBar/Input/index.jsx ================================================ import { Component, h } from 'preact'; // import '@material/textfield/dist/mdc.textfield.min.css'; import 'scss/styles.scss'; export default class Input extends Component { render() { const { placeholder, searchString, onKeyDown, onInput, onBlur } = this.props; return (
    input && input.focus()} />
    ); } } ================================================ FILE: src/saka/Main/Components/SearchBar/index.jsx ================================================ import { h } from 'preact'; import 'scss/styles.scss'; import Input from './Input/index.jsx'; export default ({ placeholder, searchString, suggestion, onKeyDown, onInput, onBlur }) => (
    ); ================================================ FILE: src/saka/Main/Components/SettingsBar/index.jsx ================================================ import { h } from 'preact'; import { colorMap } from 'lib/colors.js'; import 'scss/styles.scss'; const Item = ({ label, color }) => ( {label} ); export default () => (
    ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Components/Suggestion/index.jsx ================================================ import { h } from 'preact'; import { fadedColorMap } from 'lib/colors.js'; import { ctrlChar } from 'lib/utils.js'; import { icons } from 'suggestion_utils/index.js'; import 'scss/styles.scss'; export default ({ type, title, titleColor, secondary, secondaryColor, url, favIconUrl, incognito, selected, index, onClick }) => { const color = fadedColorMap[type]; const icon = icons[type]; const incognitoIcon = icons.incognito; let suggestionIcon; if (incognito === true) { suggestionIcon = ( ); } else if (SAKA_PLATFORM === 'chrome' && url) { suggestionIcon = (
    ); } else if (SAKA_PLATFORM === 'firefox' && favIconUrl) { suggestionIcon = ( ); } else { suggestionIcon = ( ); } return (
  • onClick(index)} onClick={() => onClick(index)} > {suggestionIcon} {title} {secondary && ( {secondary} )} {selected ? ( ) : ( `${ctrlChar}-${index + 1}` )}
  • ); }; ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/BookmarkSuggestion/index.jsx ================================================ import { h } from 'preact'; import highlight from 'lib/highlight.jsx'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title, url, matches }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/ClosedTabSuggestion/index.jsx ================================================ import { h } from 'preact'; import highlight from 'lib/highlight.jsx'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title, url, matches, favIconUrl, incognito }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/CommandSuggestion/index.jsx ================================================ import { h } from 'preact'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/HistorySuggestion/index.jsx ================================================ import { h } from 'preact'; import highlight from 'lib/highlight.jsx'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title, url, matches }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/RecentlyViewedSuggestion/index.jsx ================================================ import { h } from 'preact'; import highlight from 'lib/highlight.jsx'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title, url, matches, favIconUrl, incognito }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/SearchEngineSuggestion/index.jsx ================================================ import { h } from 'preact'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title, isURL, prettyURL }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/SuggestionSelector.jsx ================================================ import { h, Component } from 'preact'; import TabSuggestion from './TabSuggestion/index.jsx'; import ClosedTabSuggestion from './ClosedTabSuggestion/index.jsx'; import BookmarkSuggestion from './BookmarkSuggestion/index.jsx'; import HistorySuggestion from './HistorySuggestion/index.jsx'; import RecentlyViewedSuggestion from './RecentlyViewedSuggestion/index.jsx'; import CommandSuggestion from './CommandSuggestion/index.jsx'; import SearchEngineSuggestion from './SearchEngineSuggestion/index.jsx'; import UnknownSuggestion from './UnknownSuggestion/index.jsx'; export default props => { switch (props.suggestion.type) { case 'tab': return ; case 'closedTab': return ; case 'bookmark': return ; case 'history': return ; case 'recentlyViewed': return ; case 'command': return ; case 'searchEngine': return ; default: return ; } }; ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/TabSuggestion/index.jsx ================================================ import { h } from 'preact'; import highlight from 'lib/highlight.jsx'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title, url, matches, favIconUrl, incognito }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/Containers/UnknownSuggestion/index.jsx ================================================ import { h } from 'preact'; import Suggestion from '../../Components/Suggestion/index.jsx'; export default ({ suggestion: { title }, selected, index, onClick }) => ( ); ================================================ FILE: src/saka/Main/Components/SuggestionList/index.jsx ================================================ import 'material-components-web/dist/material-components-web.css'; import 'scss/styles.scss'; import { h, Component } from 'preact'; import Suggestion from './Containers/SuggestionSelector.jsx'; export default ({ searchString, suggestions, selectedIndex, firstVisibleIndex, maxSuggestions, onSuggestionClick }) => (
      {suggestions .slice(firstVisibleIndex, firstVisibleIndex + maxSuggestions) .map((suggestion, index) => { return ( ); })}
    ); ================================================ FILE: src/saka/Main/Containers/GeneralSearch/index.jsx ================================================ import { h } from 'preact'; export default () =>

    General Search

    ; ================================================ FILE: src/saka/Main/Containers/StandardSearch/index.jsx ================================================ import browser from 'webextension-polyfill'; import { Component, h } from 'preact'; import { getSuggestions, activateSuggestion, closeTab } from 'suggestion_engine/client/index.js'; import { preprocessSuggestion } from 'suggestion_utils/index.js'; import { ctrlKey } from 'lib/utils.js'; import { slowWheelEvent } from 'lib/dom.js'; import SearchBar from '../../Components/SearchBar/index.jsx'; import SuggestionList from '../../Components/SuggestionList/index.jsx'; import PaginationBar from '../../Components/PaginationBar/index.jsx'; import GUIContainer from '../../Components/GUIContainer/index.jsx'; import BackgroundImage from '../../Components/BackgroundImage/index.jsx'; import ModeSwitcher from '../../Components/ModeSwitcher/index.jsx'; // provides suggestions but doesn't autocomplete input export default class extends Component { state = { searchString: '', suggestions: [], selectedIndex: 0, // 0 <= selectedIndex < maxSuggestions firstVisibleIndex: 0, // 0 <= firstVisibleIndex < suggestion.length maxSuggestions: 6, undoIndex: this.props.searchHistory.size - 1 }; componentDidMount() { this.updateAutocompleteSuggestions('').then(() => { const { suggestions } = this.state; if (suggestions.length > 1) { this.setState({ selectedIndex: 1 }); } }); } componentDidUpdate(prevProps) { if (this.props.mode !== prevProps.mode) { this.updateAutocompleteSuggestions(this.state.searchString); } } getPreviousSearchString = () => { if (this.state.undoIndex !== 0) { this.setState({ searchString: [...this.props.searchHistory][this.state.undoIndex], undoIndex: this.state.undoIndex - 1 }); this.updateAutocompleteSuggestions(this.state.searchString); } }; getNextSearchString = () => { if (this.state.undoIndex < this.props.searchHistory.size) { this.setState({ searchString: [...this.props.searchHistory][this.state.undoIndex], undoIndex: this.state.undoIndex + 1 }); this.updateAutocompleteSuggestions(this.state.searchString); } }; handleWheel = slowWheelEvent( 50, () => { this.incrementSelectedIndex(1); }, () => { this.incrementSelectedIndex(-1); } ); handleKeyDown = e => { switch (e.key) { case 'Escape': browser.runtime.sendMessage({ key: 'closeSaka', searchHistory: [...this.props.searchHistory] }); break; case 'Backspace': if (ctrlKey(e)) { e.preventDefault(); this.closeTab(); } else if (!e.repeat && e.target.value === '') { browser.runtime.sendMessage({ key: 'closeSaka', searchHistory: [...this.props.searchHistory] }); } break; case 'ArrowLeft': case 'ArrowRight': break; case 'ArrowDown': e.preventDefault(); this.props.updateSearchHistory(this.state.searchString); this.incrementSelectedIndex(1); break; case 'ArrowUp': e.preventDefault(); this.props.updateSearchHistory(this.state.searchString); this.incrementSelectedIndex(-1); break; case 'Tab': e.preventDefault(); this.props.updateSearchHistory(this.state.searchString); e.shiftKey ? this.incrementSelectedIndex(-1) : this.incrementSelectedIndex(1); break; case '1': case '2': case '3': case '4': case '5': case '6': if (ctrlKey(e)) { e.preventDefault(); this.tryActivateSuggestion(Number.parseInt(10, e.key) - 1); } break; case 'Enter': e.preventDefault(); this.props.updateSearchHistory( this.state.searchString, this.tryActivateSuggestion ); break; case 'k': if (ctrlKey(e)) { e.preventDefault(); this.setState({ searchString: '' }); this.updateAutocompleteSuggestions(''); } break; case 's': if (ctrlKey(e)) { e.preventDefault(); this.previousPage(); } break; case 'd': if (ctrlKey(e)) { e.preventDefault(); this.nextPage(); } break; case ' ': if (e.shiftKey || this.state.searchString === '') { e.preventDefault(); this.props.shuffleMode(); } break; case 'A': if (ctrlKey(e)) { e.preventDefault(); this.props.setMode('tab'); } break; case 'C': if (ctrlKey(e)) { e.preventDefault(); this.props.setMode('closedTab'); } break; case 'M': if (ctrlKey(e)) { e.preventDefault(); this.props.setMode('mode'); } break; case 'b': if (ctrlKey(e)) { e.preventDefault(); this.props.setMode('bookmark'); } break; case 'E': if (ctrlKey(e)) { e.preventDefault(); this.props.setMode('history'); } break; case 'z': if (ctrlKey(e)) { e.preventDefault(); this.getPreviousSearchString(); } break; case 'y': if (ctrlKey(e)) { e.preventDefault(); this.getNextSearchString(); } break; case 'X': if (ctrlKey(e)) { e.preventDefault(); this.props.setMode('recentlyViewed'); } break; default: this.setState({ undoIndex: this.props.searchHistory.size - 1 }); break; } }; nextPage = () => { const { firstVisibleIndex, maxSuggestions, suggestions: { length: numSuggestions } } = this.state; const newFirstVisibleIndex = Math.max( 0, Math.min( firstVisibleIndex + maxSuggestions, numSuggestions - maxSuggestions ) ); this.setState({ firstVisibleIndex: newFirstVisibleIndex, selectedIndex: 0 }); }; previousPage = () => { const { firstVisibleIndex, maxSuggestions } = this.state; const newFirstVisibleIndex = Math.max( 0, firstVisibleIndex - maxSuggestions ); this.setState({ firstVisibleIndex: newFirstVisibleIndex, selectedIndex: 0 }); }; incrementSelectedIndex = increment => { const { selectedIndex } = this.state; this.trySetIndex(selectedIndex + increment); }; trySetIndex = index => { if (this.indexInRange(index)) { this.setState({ selectedIndex: index }); } else { const { firstVisibleIndex, maxSuggestions, suggestions } = this.state; if (index < 0 && firstVisibleIndex > 0) { this.setState({ firstVisibleIndex: firstVisibleIndex - 1 }); } else if ( index >= maxSuggestions && firstVisibleIndex + maxSuggestions < suggestions.length ) { this.setState({ firstVisibleIndex: firstVisibleIndex + 1 }); } } }; indexInRange = index => { const { suggestions, maxSuggestions } = this.state; return ( index >= 0 && index <= Math.max(0, Math.min(suggestions.length, maxSuggestions) - 1) ); }; closeTab = async (index = this.state.selectedIndex) => { const { suggestions, firstVisibleIndex } = this.state; const suggestion = suggestions[firstVisibleIndex + index]; if (suggestion && this.props.mode === 'tab') { await closeTab(suggestion); suggestions.splice(firstVisibleIndex + index, 1); this.setState({ suggestions }); } }; tryActivateSuggestion = async (index = this.state.selectedIndex) => { const { suggestions, firstVisibleIndex } = this.state; const suggestion = suggestions[firstVisibleIndex + index]; if (suggestion) { if (suggestion.type === 'mode') { this.props.setMode(suggestion.mode); } else { activateSuggestion(suggestion); await browser.runtime.sendMessage({ key: 'closeSaka', searchHistory: [...this.props.searchHistory] }); } } }; handleInput = e => { const newSearchString = e.target.value; const { oldSearchString } = this.state; this.setState({ searchString: newSearchString }); if (newSearchString !== oldSearchString) { this.setState({ selectedIndex: 0, searchString: newSearchString }); this.updateAutocompleteSuggestions(newSearchString); } }; updateAutocompleteSuggestions = async searchStringAtLookup => { const suggestions = await getSuggestions( this.props.mode, searchStringAtLookup ); const { searchString: searchStringNow } = this.state; if (searchStringNow === searchStringAtLookup) { this.setState({ suggestions: suggestions.map(suggestion => preprocessSuggestion(suggestion, searchStringAtLookup) ), firstVisibleIndex: 0, selectedIndex: 0 }); } }; handleBlur = e => { this.props.updateSearchHistory(e.target.value); }; handleButtonClick = () => { this.props.setMode('mode'); }; handleSuggestionClick = index => { this.tryActivateSuggestion(index); }; render() { const { placeholder, mode, showEmptySearchSuggestions } = this.props; const { searchString, suggestions, selectedIndex, firstVisibleIndex, maxSuggestions } = this.state; const suggestion = suggestions[firstVisibleIndex + selectedIndex]; if (!showEmptySearchSuggestions && !searchString) { return ( ); } // TODO: Rename suggestions and suggestion return ( ); } } ================================================ FILE: src/saka/Main/Containers/TabSearch/index.jsx ================================================ import { h } from 'preact'; export default () =>

    Tab Search

    ; ================================================ FILE: src/saka/Main/index.jsx ================================================ import 'material-components-web/dist/material-components-web.css'; import 'scss/styles.scss'; import browser from 'webextension-polyfill'; import { Component, h } from 'preact'; import StandardSearch from './Containers/StandardSearch/index.jsx'; export default class Main extends Component { constructor(props) { super(props); this.state = { mode: 'tab', modes: ['tab', 'closedTab', 'bookmark', 'history', 'recentlyViewed'], isLoading: true, showEmptySearchSuggestions: true, searchHistory: new Set([]) }; } async componentDidMount() { const sakaSettings = await this.fetchSakaSettings(); this.setState(sakaSettings); } setMode = mode => { this.setState({ mode }); }; shuffleMode = () => { const { mode, modes } = this.state; const nextIndex = modes.indexOf(mode) + 1; const nextModeIndex = nextIndex >= modes.length ? 0 : nextIndex; this.setMode(modes[nextModeIndex]); }; fetchSakaSettings = async function fetchSakaSettings() { const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']); let { searchHistory } = await browser.storage.sync.get(['searchHistory']); searchHistory = searchHistory !== undefined && searchHistory.length > 0 ? new Set(searchHistory) : new Set(['']); if (sakaSettings !== undefined) { const { mode, showEmptySearchSuggestions } = sakaSettings; return { isLoading: false, mode, showEmptySearchSuggestions, searchHistory }; } return { isLoading: false, searchHistory }; }; updateSearchHistory = (searchString, callback) => { const { searchHistory } = this.state; searchHistory.delete(searchString); searchHistory.add(searchString); this.setState({ searchHistory }, callback); }; render() { const { mode, isLoading, showEmptySearchSuggestions, searchHistory } = this.state; const { setMode, shuffleMode } = this; if (!isLoading) { switch (mode) { case 'tab': return ( ); case 'closedTab': return ( ); case 'bookmark': return ( ); case 'history': return ( ); case 'recentlyViewed': return ( ); default: return
    Error, invalid mode
    ; } } else { return
    ; } } } ================================================ FILE: src/saka/index.jsx ================================================ import { render, h } from 'preact'; import 'material-components-web/dist/material-components-web.css'; import Main from './Main/index.jsx'; render(
    , document.body); ================================================ FILE: src/scss/options.scss ================================================ $mdc-theme-primary: #00796b; // Purple 500 $mdc-theme-secondary: #4db6ac; // Orange A200 @import '@material/animation/functions'; @import '@material/theme/mdc-theme'; html { background-color: #eeeeee; } .options-container { background-color: #ffffff; margin: auto; position: relative; top: 50%; width: 60%; margin-top: 80px; } .options-form { padding: 10px; } .option { padding-bottom: 10px; } .options-separator { margin-bottom: 5px; color: #f1f1f1; } .options-save { margin-top: 5px; margin-right: 10px; } .options-icon { margin-top: 5px; } // http://www.jimmyscode.com/css-styling-for-kbd-tags/ kbd { padding: 0.1em 0.6em; border: 1px solid #ccc; font-size: 11px; font-family: Arial, Helvetica, sans-serif; background-color: #f7f7f7; color: #333; -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; display: inline-block; margin: 0 0.1em; text-shadow: 0 1px 0 #fff; line-height: 1.4; white-space: nowrap; } .tooltip { float: right; } .tooltip .tooltiptext { visibility: hidden; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 5px; position: absolute; z-index: 3; } .tooltip:hover .tooltiptext { visibility: visible; } .keyboard-shortcut-heading { padding-bottom: 20px; } ================================================ FILE: src/scss/styles.scss ================================================ * { box-sizing: border-box; } html, body { margin: 0; } #GUIContainer { background-color: #ffffff; position: absolute; left: 50%; right: 0; width: 680px; border-width: 0; transform-origin: 50% 0%; -webkit-box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2), 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12); box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2), 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12); overflow: hidden; } #background-image { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background-size: cover; } #pagination-bar { display: flex; flex-flow: row no-wrap; justify-content: space-around; width: 100%; height: 14px; font-size: 12px; line-height: 14px; color: gray; } .pagination-item { cursor: pointer; opacity: 0.44; } .pagination-item:hover { opacity: 1; } .paginator-next-button { flex-grow: 3; opacity: 0.6; text-align: center; cursor: pointer; } .paginator-next-button:hover { color: white; background-color: gray; } .paginator-text-info { flex-grow: 2; text-align: center; } .arrow-normalizer { font-size: 15px; line-height: 9px; margin-right: 1px; } .search-bar-container { width: 100%; display: flex; border-bottom: 1px solid rgba(0, 0, 0, 0.12); } section.search-field-wrapper { font-size: 26px; padding-right: 16px; display: flex; width: 624px; border-bottom: none; } input.search-field-input { font-size: inherit; } span.suggestion-text { align-self: center; } #search-bar { color: #000000; padding-left: 20px; } #search-bar::selection { background-color: rgba(63, 81, 245, 0.15); } #action-button { height: 56px; width: 56px; min-width: 56px; border-radius: 0; display: flex; align-items: center; justify-content: center; color: rgba(63, 81, 245, 0.6); background-color: rgba(63, 81, 245, 0); cursor: pointer; } #action-button:hover { /* color: #ffffff; background-color: rgba(63, 81, 245, 1.0); */ /* -webkit-box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); */ } #action-button > i { font-size: 32px; } #settings-bar { display: flex; flex-flow: row no-wrap; justify-content: space-around; width: 100%; height: 12px; font-size: 10px; line-height: 10px; color: rgba(0, 0, 0, 0.34); border-bottom: 1px solid rgba(0, 0, 0, 0.12); } .settings-item { cursor: pointer; opacity: 0.44; } .settings-item:hover { opacity: 1; } ul.list-container { padding: 0px; } .search-item { padding: 0px 16px; } .search-icon { color: rgba(0, 0, 0, 0.26); } .two-line-avatar-text-icon-demo .mdc-list-item__graphic { display: inline-flex; align-items: center; justify-content: center; /*color: white;*/ } kbd { color: #3f51f5; font-family: roboto, sans-serif; font-weight: normal; letter-spacing: 0.56px; -webkit-font-smoothing: antialiased; border: 1px solid #3f51f5; border-bottom: 5px solid; border-left: 3px solid; padding: 0px 4px; border-radius: 4px; border-bottom-left-radius: 3px; box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12); margin: auto 1px; } .grey-bg { background: rgba(0, 0, 0, 0.26); } span.kbd-end-detail { white-space: nowrap; color: gray; text-decoration: none; margin-right: 6px; } .search-item { padding-left: 10px !important; border-left: 6px solid; cursor: pointer; } .search-item:hover { background-color: rgb(245, 245, 245) !important; } .suggestion-wrap-text { white-space: nowrap; text-overflow: ellipsis; max-width: 530px; overflow-x: hidden; } .suggestion-icon { width: 25px; height: 25px; } .mode-switcher-wrapper { width: 100%; display: flex; flex-direction: row; border-bottom: 1px solid rgba(0, 0, 0, 0.12); } .mode-switcher-icon { flex-grow: 1; flex-wrap: nowrap; background-color: white; text-align: center; padding: 2px 0px 2px 0px; border-top: 3px solid transparent; cursor: pointer; } .mode-switcher-icon + .mode-switcher-icon { border-left: 2px solid rgba(0, 0, 0, 0.05); } .mode-switcher-icon:hover { background-color: rgb(245, 245, 245); } ================================================ FILE: src/suggestion_engine/client/index.js ================================================ import msg from 'msg/client.js'; export async function getSuggestions(mode, searchString) { return msg('sg', [mode, searchString]); } export async function activateSuggestion(suggestion) { return msg('activateSuggestion', suggestion); } export async function closeTab(suggestion) { return msg('closeTab', suggestion); } ================================================ FILE: src/suggestion_engine/server/index.js ================================================ import browser from 'webextension-polyfill'; import * as providers from './providers/index.js'; export async function getSuggestions([mode, searchString]) { return providers[mode](searchString); } async function focusOrCreateTab(url) { const matchingTabs = await browser.tabs.query({ url }); if (matchingTabs && matchingTabs.length > 0) { // If multiple matching tabs then just focus the first one const existingTab = matchingTabs[0]; await browser.tabs.update(existingTab.id, { active: true }); await browser.windows.update(existingTab.windowId, { focused: true }); } else { await browser.tabs.create({ url }); } } export async function activateSuggestion(suggestion) { switch (suggestion.type) { case 'tab': await browser.tabs.update(suggestion.tabId, { active: true }); await browser.windows.update(suggestion.windowId, { focused: true }); break; case 'closedTab': await browser.sessions.restore(suggestion.sessionId); break; case 'bookmark': await focusOrCreateTab(suggestion.url); break; case 'history': await focusOrCreateTab(suggestion.url); break; case 'recentlyViewed': await activateSuggestion({ ...suggestion, type: suggestion.originalType }); break; default: console.error( `activation not yet implemented for suggestions of type ${ suggestion.type }` ); } } export async function closeTab(suggestion) { await browser.tabs.remove(suggestion.tabId); } ================================================ FILE: src/suggestion_engine/server/providers/bookmark.js ================================================ import browser from 'webextension-polyfill'; import { isURL, extractProtocol, isProtocol } from 'lib/url.js'; import { getFilteredSuggestions } from 'lib/utils.js'; // https://github.com/nwjs/chromium.src/blob/45886148c94c59f45f14a9dc7b9a60624cfa626a/components/omnibox/browser/bookmark_provider.cc async function allBookmarkSuggestions(searchText) { const searchCriteria = searchText === '' ? {} : searchText; const searchResults = await browser.bookmarks.search(searchCriteria); const validResults = []; searchResults.forEach(({ url, title }) => { const protocol = extractProtocol(url); if (isURL(url) && isProtocol(protocol)) { validResults.push({ type: 'bookmark', score: -1, title, url }); } }); return validResults; } export default async function bookmarkSuggestions(searchString) { const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']); const enableFuzzySearch = sakaSettings && sakaSettings.enableFuzzySearch !== undefined ? sakaSettings.enableFuzzySearch : true; if (searchString && enableFuzzySearch) { return getFilteredSuggestions(searchString, { getSuggestions: allBookmarkSuggestions, threshold: 1, keys: ['title', 'url'] }); } return allBookmarkSuggestions(searchString); } ================================================ FILE: src/suggestion_engine/server/providers/closedTab.js ================================================ import browser from 'webextension-polyfill'; import { isSakaUrl } from 'lib/url.js'; // import { filter } from 'rxjs/operator/filter'; import { allTabSuggestions } from './tab.js'; import { getFilteredSuggestions } from 'lib/utils.js'; export async function getAllSuggestions() { const sessions = await browser.sessions.getRecentlyClosed(); const filteredSessions = []; // TODO: This for loop is currently flagged by the airbnb eslint rules. // See: https://github.com/airbnb/javascript/issues/1271 // Not disabling the rule as this might be fixable in the future using filter. // This for loop is needed at the moment as a workaround since filter does not support async. for (const session of sessions) { if (session.tab && !(await isSakaUrl(session.tab.url))) { filteredSessions.push(session); } else if (session.window && session.window.tabs) { for (const tabSession of session.window.tabs) { if (tabSession && !(await isSakaUrl(tabSession.url))) { filteredSessions.push({ lastModified: session.window.lastModified, tab: { ...tabSession, sessionId: session.window.sessionId } }); } } } } return filteredSessions.map(session => { const { lastModified } = session; const { id, sessionId, title, url, favIconUrl, incognito } = session.tab; return { type: 'closedTab', tabId: id, sessionId, score: undefined, title, url, favIconUrl: incognito ? null : favIconUrl, incognito, lastAccessed: lastModified }; }); } // TODO: Remove when Chrome gets proper timestamp export async function recentlyClosedTabSuggestions() { const { recentlyClosed } = await browser.runtime.getBackgroundPage(); const sessions = await browser.sessions.getRecentlyClosed(); const filteredSessions = []; // TODO: This for loop is currently flagged by the airbnb eslint rules. // See: https://github.com/airbnb/javascript/issues/1271 // Not disabling the rule as this might be fixable in the future using filter. // This for loop is needed at the moment as a workaround since filter does not support async. for (const session of sessions) { if (session.tab && !(await isSakaUrl(session.tab.url))) { filteredSessions.push(session); } } return filteredSessions .map(session => { const foundTab = recentlyClosed.findIndex(tab => { return tab.tabId === session.tab.id; }); if (foundTab !== -1) { return { ...session, lastModified: recentlyClosed.tab.lastAccessed }; } return session; }) .map(session => { const { lastModified } = session; const { id, sessionId, title, url, favIconUrl, incognito } = session.tab; return { type: 'closedTab', tabId: id, sessionId, score: undefined, title, url, favIconUrl: incognito ? null : favIconUrl, incognito, lastAccessed: lastModified }; }); } export default async function closedTabSuggestions(searchString) { return searchString === '' ? getAllSuggestions() : getFilteredSuggestions(searchString, { getSuggestions: getAllSuggestions, threshold: 0.5, keys: ['title', 'url'] }); } ================================================ FILE: src/suggestion_engine/server/providers/command.js ================================================ import { MAX_RESULTS } from './index.js'; const commands = ['search', 'help', 'history', 'tabs', 'define']; export default function commandSuggestions(searchText) { return commands .filter(command => command.startsWith(searchText)) .slice(0, MAX_RESULTS) .map(command => ({ type: 'command', score: -1, title: command })); } ================================================ FILE: src/suggestion_engine/server/providers/history.js ================================================ import browser from 'webextension-polyfill'; import { isSakaUrl } from 'lib/url.js'; import { getFilteredSuggestions } from 'lib/utils.js'; export async function allHistorySuggestions(searchText) { const results = await browser.history.search({ text: searchText }); const filteredResults = []; for (const result of results) { const sakaUrl = await isSakaUrl(result.url); !sakaUrl ? filteredResults.push(result) : null; } return filteredResults.map( ({ url, title, lastVisitTime, visitCount, typedCount }) => ({ type: 'history', score: visitCount + typedCount, lastAccessed: lastVisitTime * 0.001, title, url }) ); } export default async function historySuggestions(searchString) { const { sakaSettings } = await browser.storage.sync.get(['sakaSettings']); const enableFuzzySearch = sakaSettings && sakaSettings.enableFuzzySearch !== undefined ? sakaSettings.enableFuzzySearch : true; if (searchString && enableFuzzySearch) { return getFilteredSuggestions(searchString, { getSuggestions: allHistorySuggestions, threshold: 1, keys: ['title', 'url'] }); } return allHistorySuggestions(searchString); } ================================================ FILE: src/suggestion_engine/server/providers/index.js ================================================ export { default as tab } from './tab.js'; export { default as closedTab } from './closedTab.js'; export { default as mode } from './mode.js'; // export { default as commands } from './commands'; export { default as history } from './history.js'; export { default as bookmark } from './bookmark.js'; export { default as recentlyViewed } from './recentlyViewed.js'; // export { default as searchEngine } from './searchEngine'; ================================================ FILE: src/suggestion_engine/server/providers/mode.js ================================================ import { ctrlChar } from 'lib/utils.js'; import { colorMap, fadedColorMap } from 'lib/colors.js'; import Fuse from 'fuse.js'; export const suggestions = [ { type: 'mode', mode: 'tab', label: 'Tabs', shortcut: `${ctrlChar}-shift-a`, color: colorMap.tab, fadedColor: fadedColorMap.tab, icon: 'tab' }, { type: 'mode', mode: 'closedTab', label: 'Recently Closed Tabs', shortcut: `${ctrlChar}-shift-c`, color: colorMap.closedTab, fadedColor: fadedColorMap.closedTab, icon: 'restore_page' }, { type: 'mode', label: 'Bookmarks', mode: 'bookmark', shortcut: `${ctrlChar}-b`, color: colorMap.bookmark, fadedColor: fadedColorMap.bookmark, icon: 'bookmark_border' }, { type: 'mode', label: 'History', mode: 'history', shortcut: `${ctrlChar}-shift-e`, color: colorMap.history, fadedColor: fadedColorMap.history, icon: 'history' }, { type: 'mode', label: 'Recently Viewed', mode: 'recentlyViewed', shortcut: `${ctrlChar}-shift-x`, color: colorMap.recentlyViewed, fadedColor: fadedColorMap.recentlyViewed, icon: 'timelapse' } ]; const fuse = new Fuse(suggestions, { shouldSort: true, threshold: 1.0, includeMatches: true, keys: ['label'] }); export default async function modeSuggestions(searchString) { return searchString === '' ? suggestions : fuse.search(searchString).map(({ item, matches, score }) => ({ ...item, score, matches })); } ================================================ FILE: src/suggestion_engine/server/providers/recentlyViewed.js ================================================ import { getFilteredSuggestions } from 'lib/utils.js'; import tabSuggestions, { allTabSuggestions, recentVisitedTabSuggestions } from './tab.js'; import { getAllSuggestions as getAllClosedTabs, recentlyClosedTabSuggestions } from './closedTab.js'; import { allHistorySuggestions as getAllHistoryTabs } from './history.js'; function compareRecentlyViewedSuggestions(suggestion1, suggestion2) { return suggestion2.lastAccessed - suggestion1.lastAccessed; } async function allRecentlyViewedSuggestions(searchString) { const historyTabs = await getAllHistoryTabs(searchString); let openTabs = null; let closedTabs = null; if (SAKA_PLATFORM === 'chrome') { openTabs = await recentVisitedTabSuggestions(searchString); closedTabs = await recentlyClosedTabSuggestions(searchString); } else { openTabs = await tabSuggestions(searchString); closedTabs = await getAllClosedTabs(searchString); } const filteredClosedTabs = closedTabs.filter(tab => openTabs.every(openTab => openTab.url !== tab.url) ); const filteredHistoryTabs = historyTabs.filter(tab => [...openTabs, ...filteredClosedTabs].every( openOrClosedTab => openOrClosedTab.url !== tab.url ) ); return [...openTabs, ...filteredClosedTabs, ...filteredHistoryTabs] .map(tab => ({ ...tab, originalType: tab.type, type: 'recentlyViewed' })) .sort(compareRecentlyViewedSuggestions); } async function filteredRecentlyViewedSuggestions(searchString) { const tabs = await allTabSuggestions(); const closedTabs = await getAllClosedTabs(searchString); const historyTabs = await getAllHistoryTabs(searchString); return [ ...tabs, ...Object.values(closedTabs), ...Object.values(historyTabs) ].map(tab => ({ ...tab, originalType: tab.type, type: 'recentlyViewed' })); } async function getFilteredRecentlyViewedSuggestions(searchString) { const filteredSuggestions = await getFilteredSuggestions(searchString, { getSuggestions: filteredRecentlyViewedSuggestions, threshold: 0.5, keys: ['title', 'url'] }); return filteredSuggestions.filter( (suggestion, index) => filteredSuggestions.findIndex( filteredSuggestion => filteredSuggestion.url === suggestion.url && filteredSuggestion.title === suggestion.title ) === index ); } export default async function recentlyViewedSuggestions(searchString) { if (searchString === '') { return allRecentlyViewedSuggestions(searchString, SAKA_PLATFORM); } return getFilteredRecentlyViewedSuggestions(searchString); } ================================================ FILE: src/suggestion_engine/server/providers/searchEngine.js ================================================ import { MAX_RESULTS } from './index.js'; export default async function searchEngineSuggestions(searchText) { try { const baseURL = 'https://www.google.com/complete/search?client=chrome-omni&q='; const response = await fetch(`${baseURL}${encodeURIComponent(searchText)}`); const json = await response.json(); return json[1].slice(0, MAX_RESULTS).map(result => ({ type: 'searchEngine', score: -1, title: result })); } catch (e) { return []; } } ================================================ FILE: src/suggestion_engine/server/providers/tab.js ================================================ import browser from 'webextension-polyfill'; import { getFilteredSuggestions, objectFromArray } from 'lib/utils.js'; export async function allTabSuggestions() { const tabs = await browser.tabs.query({}); return tabs.map( ({ id: tabId, windowId, title, url, favIconUrl, incognito, lastAccessed }) => ({ type: 'tab', tabId, windowId, title, url, favIconUrl: incognito ? null : favIconUrl, incognito, lastAccessed: lastAccessed * 0.001 }) ); } async function recentTabSuggestions() { const tabs = await allTabSuggestions(); const tabsMap = objectFromArray(tabs, 'tabId'); const { tabHistory } = await browser.runtime.getBackgroundPage(); const recentTabs = tabHistory.map(recentlyUsedTab => { const tab = tabsMap[recentlyUsedTab.tabId]; delete tabsMap[recentlyUsedTab.tabId]; return { ...tab, lastAccessed: recentlyUsedTab.lastAccessed }; }); return [...recentTabs, ...Object.values(tabsMap)]; } // TODO: Remove this once chrome tab API provides recently viewed export async function recentVisitedTabSuggestions() { const tabs = await allTabSuggestions(); const tabsMap = objectFromArray(tabs, 'tabId'); const { tabHistory } = await browser.runtime.getBackgroundPage(); const recentTabs = tabHistory.map(recentlyUsedTab => { const tab = tabsMap[recentlyUsedTab.tabId]; delete tabsMap[recentlyUsedTab.tabId]; return { ...tab, lastAccessed: recentlyUsedTab.lastAccessed * 0.001 }; }); return [...recentTabs]; } export default async function tabSuggestions(searchString) { return searchString === '' ? recentTabSuggestions() : getFilteredSuggestions(searchString, { getSuggestions: allTabSuggestions, threshold: 0.5, keys: ['title', 'url'] }); } ================================================ FILE: src/suggestion_utils/index.js ================================================ import { prettifyURL, isURL } from 'lib/url.js'; export const icons = { mode: 'apps', tab: 'tab', closedTab: 'restore_page', history: 'history', recentlyViewed: 'timelapse', bookmark: 'bookmark_border', incognito: 'visibility_off' }; export function preprocessSuggestion(suggestion, searchText) { switch (suggestion.type) { case 'tab': { const prettyURL = prettifyURL(suggestion.url, searchText); return { ...suggestion, prettyURL, text: suggestion.title }; } case 'closedTab': { const prettyURL = prettifyURL(suggestion.url, searchText); return { ...suggestion, prettyURL, text: suggestion.title }; } case 'mode': return suggestion; case 'bookmark': { const prettyURL = prettifyURL(suggestion.url, searchText); return { ...suggestion, prettyURL, text: prettyURL }; } case 'history': { const prettyURL = prettifyURL(suggestion.url, searchText); return { ...suggestion, prettyURL, text: prettyURL }; } case 'recentlyViewed': { const prettyURL = prettifyURL(suggestion.url, searchText); return { ...suggestion, prettyURL, text: prettyURL }; } default: return { type: 'error', title: `Error. Unknown Suggestion type: ${suggestion.type}`, text: `Error. Unknown Suggestion type: ${suggestion.type}` }; } } ================================================ FILE: static/background_page.html ================================================ ================================================ FILE: static/material-icons.css ================================================ @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: local('Material Icons'), local('MaterialIcons-Regular'), url(./MaterialIcons-Regular.woff2) format('woff2'); } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 24px; /* Preferred icon size */ display: inline-block; line-height: 1; text-transform: none; letter-spacing: normal; word-wrap: normal; white-space: nowrap; direction: ltr; /* Support for all WebKit browsers. */ -webkit-font-smoothing: antialiased; /* Support for Safari and Chrome. */ text-rendering: optimizeLegibility; /* Support for Firefox. */ -moz-osx-font-smoothing: grayscale; /* Support for IE. */ font-feature-settings: 'liga'; } ================================================ FILE: static/options.html ================================================ Saka Options ================================================ FILE: static/saka.html ================================================ Saka ================================================ FILE: test/Icon.test.js ================================================ import Icon from '../src/saka/Main/Components/Icon/index.jsx'; import { render } from 'preact-render-spy'; import { h } from 'preact'; describe('Icon component ', () => { it('should render while enabled', () => { const props = { icon: '', color: 'rgba(1,1,1,0.44)' }; const iconRender = render(); const icon = iconRender.find('#icon'); expect(icon).toBeTruthy(); }); }); ================================================ FILE: test/Main.test.js ================================================ import { h } from 'preact'; import { render, cleanup, wait, fireEvent, flushPromises } from 'preact-testing-library'; import 'jest-dom/extend-expect'; import Main from '@/saka/Main/index.jsx'; beforeEach(() => { browser.flush(); // browser.storage.sync.get.returns({ // sakaSettings: { // mode: 'tab', // showEmptySearchSuggestions: false // }, // searchHistory: [] // }); browser.storage.local.get.resolves( Promise.resolve({ screenshot: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAPWCAYAAAABOoU/AAAgAElEQVR4nOzdeXzbd2H/8e9XcqDOXdrC2OAH/W1l49rooMAgvQsbg0F}' }) ); browser.storage.local.remove.returns(''); browser.runtime.sendMessage.returns(''); }); test('should show all options when not showing key bindings', async () => { render(
    ); }); afterEach(cleanup); ================================================ FILE: test/ModeSwitcher.test.js ================================================ import ModeSwitcher from '@/saka/Main/Components/ModeSwitcher/index.jsx'; import { render, cleanup, fireEvent, flushPromises } from 'preact-testing-library'; import { fadedColorMap } from 'lib/colors.js'; import { h } from 'preact'; describe('ModeSwitcher component ', () => { it('should render tabs with selected tab colored, rest of tabs gray', async () => { const setMode = jest.fn(); const props = { mode: 'tab', setMode }; const { getByText } = render(); expect(getByText('tab')).toMatchSnapshot(); expect(getByText('restore_page')).toMatchSnapshot(); expect(getByText('bookmark_border')).toMatchSnapshot(); expect(getByText('history')).toMatchSnapshot(); expect(getByText('timelapse')).toMatchSnapshot(); fireEvent.click(getByText('restore_page'), 'click'); await flushPromises(); expect(setMode.mock.calls.length).toBe(1); }); }); afterEach(cleanup); ================================================ FILE: test/PaginationBar.test.js ================================================ import PaginationBar from '@/saka/Main/Components/PaginationBar/index.jsx'; import { render, getByText } from 'preact-testing-library'; import { h } from 'preact'; describe('PaginationBar component ', () => { it('should be empty when no there are no suggestions', () => { const props = { firstVisibleIndex: 0, suggestions: [], maxSuggestions: 6, onClickPrevious() {}, onClickNext() {} }; const { queryByText } = render(); expect(queryByText('◄')).toBeNull(); expect(queryByText('ctrl-S')).toBeNull(); expect(queryByText('ctrl-D ►')).toBeNull(); }); it('should show correct amount of suggestions when there are suggestions found', () => { const props = { firstVisibleIndex: 0, suggestions: [ { type: 'tab', title: 'lusakasa/saka: Elegant tab search', url: 'https://github.com/lusakasa/saka' }, { type: 'tab', title: 'Google', url: 'https://google.com' } ], maxSuggestions: 6, onClickPrevious() {}, onClickNext() {} }; const { getByText } = render(); getByText('◄'); getByText('ctrl-S'); getByText('1 - 2 / 2'); getByText('ctrl-D ►'); }); }); ================================================ FILE: test/SearchBar.test.js ================================================ import { h } from 'preact'; import { render, cleanup, flushPromises } from 'preact-testing-library'; import SearchBar from '@/saka/Main/Components/SearchBar/index'; afterEach(cleanup); test('should be empty when no there is no search string provided', async () => { const props = { placeholder: 'Tabs', searchString: '', suggestion: {}, mode: 'tab', onKeyDown() {}, onInput() {}, onBlur() {}, onButtonClick() {} }; const { container } = render(); expect(container).toMatchSnapshot(); }); test('should show the search string when search string is provided', async () => { const props = { placeholder: 'Tabs', searchString: 'Saka github', suggestion: {}, mode: 'tab', onKeyDown() {}, onInput() {}, onBlur() {}, onButtonClick() {} }; const { getByPlaceholderText } = render(); expect(getByPlaceholderText('Tabs').value).toBe('Saka github'); }); ================================================ FILE: test/StandardSearch/StandardSearch.test.js ================================================ import { h } from 'preact'; import { render, cleanup, flushPromises, fireEvent, wait } from 'preact-testing-library'; import StandardSearch from '@/saka/Main/Containers/StandardSearch/index.jsx'; beforeEach(() => { browser.flush(); browser.storage.local.get.resolves( Promise.resolve({ screenshot: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAPWCAYAAAABOoU/AAAgAElEQVR4nOzdeXzbd2H/8e9XcqDOXdrC2OAH/W1l49rooMAgvQsbg0F}' }) ); browser.storage.local.remove.returns(''); browser.runtime.sendMessage.returns(''); }); test('should not show suggestion list when showEmptySearchSuggestions false and no search string', () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: false, searchHistory: ['first', 'second', 'third'], updateSearchHistory: jest.fn() }; const { getByPlaceholderText } = render(); expect(getByPlaceholderText('Tabs').value).toBe(''); }); test('should render and allow user input to search for suggestion', () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: [], updateSearchHistory: jest.fn(), shuffleMode: jest.fn() }; const { getByPlaceholderText } = render(); getByPlaceholderText('Tabs').value = 'Test input'; fireEvent.input(getByPlaceholderText('Tabs')); expect(getByPlaceholderText('Tabs').value).toBe('Test input'); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: ' ', shiftKey: true }); expect(props.shuffleMode.mock.calls.length).toBe(1); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'k', keyCode: 75, which: 75, ctrlKey: true }); }); test('should allow keyboard navigation of Saka search results', () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: [], updateSearchHistory: jest.fn() }; const { getByPlaceholderText } = render(); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'Tab' }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'Tab', keyCode: 9, which: 9, shiftKey: true }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'd', keyCode: 68, which: 68, shiftKey: true, ctrlKey: true }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 's', keyCode: 83, which: 83, shiftKey: true, ctrlKey: true }); }); test('should allow going back and forward through search history', () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: ['first', 'second', 'third'], updateSearchHistory: jest.fn() }; const { getByPlaceholderText } = render(); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'z', keyCode: 90, which: 90, ctrlKey: true }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'y', keyCode: 89, which: 89, ctrlKey: true }); }); test('should allow switching between search modes', async () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: ['first', 'second', 'third'], updateSearchHistory: jest.fn(), shuffleMode: jest.fn(), setMode: jest.fn() }; const { getByPlaceholderText } = render(); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: ' ' }); expect(props.shuffleMode.mock.calls.length).toBe(1); // Recently Closed fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'C', ctrlKey: true }); // Tabs fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'A', ctrlKey: true }); // Bookmarks fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'b', ctrlKey: true }); // History fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'E', ctrlKey: true }); // Recently Viewed fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'X', ctrlKey: true }); // Modes // TODO: Deprecate this feature fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'M', ctrlKey: true }); expect(props.setMode.mock.calls.length).toBe(6); }); test('should close saka on Enter key', () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: [], updateSearchHistory: jest.fn(), shuffleMode: jest.fn() }; const { getByPlaceholderText } = render(); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'Enter' }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'Backspace' }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'Escape' }); }); test('should allow navigation via arrow keys', () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: [], updateSearchHistory: jest.fn(), shuffleMode: jest.fn() }; const { getByPlaceholderText } = render(); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'ArrowLeft' }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'ArrowRight' }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'ArrowDown' }); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'ArrowUp' }); }); test('should select suggestion based on key press', () => { const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: [], updateSearchHistory: jest.fn(), shuffleMode: jest.fn() }; const { getByPlaceholderText } = render(); [1, 2, 3, 4, 5, 6].map(num => { fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: `${num}`, ctrlKey: true }); }); }); test('should close tab and delete suggestion when key pressed', async () => { browser.tabs.query.returns([ { title: 'adjksdhk', url: 'adasd', incgnito: false }, { title: 'adjksdhk', url: 'adasd', incgnito: false } ]); const props = { placeholder: 'Tabs', mode: 'tab', showEmptySearchSuggestions: true, searchHistory: [], updateSearchHistory: jest.fn(), shuffleMode: jest.fn() }; const { getByPlaceholderText } = render(); await flushPromises(); fireEvent.keyDown(getByPlaceholderText('Tabs'), { key: 'Backspace', ctrlKey: true }); }); afterEach(cleanup); ================================================ FILE: test/Suggestion.test.js ================================================ import { h } from 'preact'; import { render, cleanup, fireEvent, flushPromises } from 'preact-testing-library'; import Suggestion from '@/saka/Main/Components/SuggestionList/Components/Suggestion'; test('should render when props passed in', async () => { global.SAKA_PLATFORM = 'chrome'; const props = { type: 'tab', title: 'Test Title', titleColor: 'fafafa', secondary: 'Secondary Test Title', secondaryColor: 'ffffff', url: 'https://example.com', favIconUrl: 'localhost:1234/path/to/icon', incognito: false, selected: 'false', index: 0, onClick: () => {} }; const { getByText } = render(); getByText('Test Title'); getByText('Secondary Test Title'); getByText('tab'); }); test('should call onClick when onClick or onKeyPress event', async () => { global.SAKA_PLATFORM = 'chrome'; const onClick = jest.fn(); const props = { type: 'tab', title: 'Test Title', titleColor: 'fafafa', secondary: 'Secondary Test Title', secondaryColor: 'ffffff', url: 'https://example.com', favIconUrl: 'localhost:1234/path/to/icon', incognito: false, selected: 'false', index: 0, onClick }; const { getByText } = render(); fireEvent.click(getByText('Test Title')); fireEvent.keyPress(getByText('Test Title')); await flushPromises(); expect(onClick.mock.calls.length).toBe(2); }); test('should hide icon when suggestion is from incognito', () => { global.SAKA_PLATFORM = 'chrome'; const onClick = jest.fn(); const props = { type: 'tab', title: 'Test Title', titleColor: 'fafafa', secondary: 'Secondary Test Title', secondaryColor: 'ffffff', url: 'https://example.com', favIconUrl: 'localhost:1234/path/to/icon', incognito: true, selected: 'false', index: 0, onClick }; const { getByText } = render(); }); test('should use correct favicon path when using firefox', () => { global.SAKA_PLATFORM = 'firefox'; const onClick = jest.fn(); const props = { type: 'tab', title: 'Test Title', titleColor: 'fafafa', secondary: 'Secondary Test Title', secondaryColor: 'ffffff', url: 'https://example.com', favIconUrl: 'localhost:1234/path/to/icon', incognito: false, selected: 'false', index: 0, onClick }; const { getByText } = render(); expect(getByText('tab')).toMatchSnapshot(); }); test('should use default favicon when no url to favicon', () => { global.SAKA_PLATFORM = 'firefox'; const onClick = jest.fn(); const props = { type: 'tab', title: 'Test Title', titleColor: 'fafafa', secondary: 'Secondary Test Title', secondaryColor: 'ffffff', incognito: false, selected: 'false', index: 0, onClick }; const { getByText } = render(); expect(getByText('tab')).toMatchSnapshot(); }); afterEach(cleanup); ================================================ FILE: test/SuggestionList.test.js ================================================ import SuggestionList from '@/saka/Main/Components/SuggestionList/index.jsx'; import { render } from 'preact-testing-library'; import { h } from 'preact'; const MAX_SUGGESTIONS = 6; beforeEach(() => { // Clears the database and adds some testing data. // Jest will wait for this promise to resolve before running tests. global.SAKA_PLATFORM = 'chrome'; }); describe('SuggestionList component ', () => { it('should be empty when no values provided', () => { const suggestions = []; const searchString = {}; const { container } = render( ); expect(container).toMatchSnapshot(); }); it('should display suggestions when provided', () => { const suggestions = [ { type: 'tab', title: 'lusakasa/saka: Elegant tab search', url: 'https://github.com/lusakasa/saka' }, { type: 'tab', title: 'Google', url: 'https://google.com' } ]; const searchString = ''; const { getByText } = render( {}} /> ); suggestions.map(suggestion => { getByText(suggestion.title); getByText(suggestion.url); }); }); }); ================================================ FILE: test/__mocks__/browser-mocks.js ================================================ const chrome = require('sinon-chrome/extensions'); const browser = require('sinon-chrome/webextensions'); global.chrome = chrome; global.browser = browser; jest.mock('msgx/client.js', () => jest.fn().mockImplementation(() => { return jest.fn().mockImplementation(mode => { let api = { zoom: Promise.resolve(1), sg: [] }; return api[mode]; }); }) ); // ({ default: jest.fn() }) ================================================ FILE: test/__mocks__/styleMock.scss ================================================ ================================================ FILE: test/__snapshots__/ModeSwitcher.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ModeSwitcher component should render tabs with selected tab colored, rest of tabs gray 1`] = ` `; exports[`ModeSwitcher component should render tabs with selected tab colored, rest of tabs gray 2`] = ` `; exports[`ModeSwitcher component should render tabs with selected tab colored, rest of tabs gray 3`] = ` `; exports[`ModeSwitcher component should render tabs with selected tab colored, rest of tabs gray 4`] = ` `; exports[`ModeSwitcher component should render tabs with selected tab colored, rest of tabs gray 5`] = ` `; ================================================ FILE: test/__snapshots__/SearchBar.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should be empty when no there is no search string provided 1`] = `
    `; ================================================ FILE: test/__snapshots__/Suggestion.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should use correct favicon path when using firefox 1`] = ` `; exports[`should use default favicon when no url to favicon 1`] = ` `; ================================================ FILE: test/__snapshots__/SuggestionList.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SuggestionList component should be empty when no values provided 1`] = `
    `; ================================================ FILE: test/lib/hightlight.test.js ================================================ import highlight from 'lib/highlight'; test('should return source text when matches is undefined', async () => { const text = 'http://www.example.com'; const key = ''; const matches = undefined; const result = highlight(text, key, matches); expect(result).toBe(text); }); test('should return source text when matches is defined but empty', async () => { const text = 'http://www.example.com'; const key = ''; const matches = []; const result = highlight(text, key, matches); expect(result).toBe(text); }); test('should return highlighted text when matches is defined and not empty', async () => { const text = 'http://www.example.com'; const key = 'title'; const matches = [ { indices: [[13, 16]], value: 'example', key: 'title', arrayIndex: 0 } ]; const result = highlight(text, key, matches); expect(result).toEqual([ 'http://www.ex', { attributes: { style: { 'font-weight': 'bold' } }, children: ['ampl'], key: undefined, nodeName: 'span' }, 'e.com' ]); }); ================================================ FILE: test/lib/log.test.js ================================================ import log from 'lib/log'; test('should not log when debug mode is disabled', async () => { global.SAKA_DEBUG = false; global.console = { log: jest.fn() }; const result = log(1, 2, 3, 4); expect(result).toBe(1); expect(global.console.log.mock.calls.length).toBe(0); }); test('should log when debug mode is enabled', async () => { global.SAKA_DEBUG = true; global.console = { log: jest.fn() }; const result = log(1, 2, 3, 4); expect(result).toBe(1); expect(global.console.log.mock.calls.length).toBe(1); }); ================================================ FILE: test/lib/url.test.js ================================================ const browser = require('sinon-chrome/webextensions'); import * as libUrl from 'lib/url'; describe('lib/url ', () => { beforeAll(() => { global.browser = browser; }); beforeEach(() => { browser.flush(); }); describe('isURL ', () => { it('should return false when URL is empty', () => { expect(libUrl.isURL('')).toBe(false); expect(libUrl.isURL(undefined)).toBe(false); expect(libUrl.isURL(null)).toBe(false); }); it('should return false when URL is invalid', () => { const url = 'http:'; expect(libUrl.isURL(url)).toBe(false); }); it('should return true when URL is valid with standard protocol', () => { expect(libUrl.isURL('https://github.com/lusakasa/saka')).toBe(true); expect(libUrl.isURL('ftp://localhost')).toBe(true); expect(libUrl.isURL('file://home/testing')).toBe(true); }); it('should return true when URL is valid with custom protocol', () => { expect(libUrl.isURL('about:blank')).toBe(true); expect(libUrl.isURL('chrome:addons')).toBe(true); }); }); describe('extractProtocol ', () => { it('should empty string when no protocol provided', () => { expect(libUrl.extractProtocol('this is a string with no protocol')).toBe( '' ); }); it('should return protocol when provided in a normal URL', () => { expect(libUrl.extractProtocol('https://github.com/lusakasa/saka')).toBe( 'https:' ); }); it('should return protocol when provided URL with port', () => { expect(libUrl.extractProtocol('https://localhost:1234')).toBe('https:'); }); }); describe('stripProtocol ', () => { it('should strip nothing when no protocol in string', () => { expect(libUrl.stripProtocol('string with no url')).toBe( 'string with no url' ); }); it('should strip protocol when given a valid url', () => { expect(libUrl.stripProtocol('https://github.com/lusakasa/saka')).toBe( 'github.com/lusakasa/saka' ); }); it('should strip protocol when given a valid url with ports', () => { expect(libUrl.stripProtocol('https://localhost:12345')).toBe( 'localhost:12345' ); }); }); describe('stripWWW ', () => { it('should strip nothing when no www in string', () => { expect(libUrl.stripWWW('https://github.com/lusakasa/saka')).toBe( 'https://github.com/lusakasa/saka' ); }); it('should strip www when provided url with www', () => { expect(libUrl.stripWWW('www.github.com/lusakasa/saka')).toBe( 'github.com/lusakasa/saka' ); }); it('should not strip www when provided url with protocol', () => { expect(libUrl.stripWWW('https://www.github.com/lusakasa/saka')).toBe( 'https://www.github.com/lusakasa/saka' ); }); }); describe('startsWithProtocol ', () => { it('should return false when does not start with a protocol', () => { expect(libUrl.startsWithProtocol('github.com/lusakasa/saka')).toBe(false); }); it('should return false when does not start with a protocol and has a port', () => { expect(libUrl.startsWithProtocol('localhost:12345')).toBe(false); }); it('should return true when does start with a protocol', () => { expect( libUrl.startsWithProtocol('https://github.com/lusakasa/saka') ).toBe(true); }); it('should return true when does start with a protocol and has a port', () => { expect(libUrl.startsWithProtocol('https://localhost:12345')).toBe(true); }); }); describe('startsWithWWW ', () => { it('should return false when does not start with a www', () => { expect(libUrl.startsWithWWW('github.com/lusakasa/saka')).toBe(false); }); it('should return false when does not start with a www and has a port', () => { expect(libUrl.startsWithWWW('myserver:12345')).toBe(false); }); it('should return true when does start with a www', () => { expect(libUrl.startsWithWWW('www.github.com/lusakasa/saka')).toBe(true); }); it('should return true when does start with a www and has a port', () => { expect(libUrl.startsWithWWW('www.myserver:12345')).toBe(true); }); }); describe('isTLD ', () => { it('should return false when not given a valid tld', () => { expect(libUrl.isTLD('faketld.test')).toBe(false); }); it('should return true when given a valid tld', () => { expect(libUrl.isTLD('com')).toBe(true); }); }); describe('isProtocol ', () => { it('should return false when not given a valid protocol', () => { expect(libUrl.isProtocol('fakeprotocol:')).toBe(false); }); it('should return true when given a valid protocol', () => { expect(libUrl.isProtocol('https:')).toBe(true); }); }); describe('isLikeURL ', () => { it('should return false when not given a url like string', () => { expect(libUrl.isLikeURL('nonurlstring')).toBe(false); }); it('should return true when given a url like string', () => { expect(libUrl.isLikeURL('https://github.com/lusakasa/saka')).toBe(true); }); it('should return true when given an ip address', () => { expect(libUrl.isLikeURL('127.0.0.1')).toBe(true); }); }); describe('prettifyURL ', () => { it('should return empty string when input empty string', () => { expect(libUrl.prettifyURL('', '')).toBe(''); }); it('should return prettified string when input prettified url', () => { expect(libUrl.prettifyURL('github.com/lusakasa/saka', '')).toBe( 'github.com/lusakasa/saka' ); }); it('should return prettified string when input url', () => { expect(libUrl.prettifyURL('http://github.com/lusakasa/saka/', '')).toBe( 'github.com/lusakasa/saka' ); }); }); describe('isSakaUrl ', () => { it('should return false when URL is empty', async () => { const sakaId = 'abcdefg/saka.html'; browser.runtime.getURL.returns(sakaId); expect(await libUrl.isSakaUrl()).toBe(false); }); it('should return false when URL does not contain saka ID', async () => { const sakaId = 'abcdefg/saka.html'; browser.runtime.getURL.returns(sakaId); expect(await libUrl.isSakaUrl('https://github.com/lusakasa')).toBe(false); }); it('should return true when URL contains saka ID', async () => { const sakaId = 'abcdefg/saka.html'; browser.runtime.getURL.returns(sakaId); expect(await libUrl.isSakaUrl('http://abcdefg/saka.html')).toBe(true); }); }); afterAll(() => { browser.flush(); delete global.browser; }); }); ================================================ FILE: test/lib/utils.test.js ================================================ import * as libUtil from 'lib/utils'; describe('lib/util ', () => { describe('objectFromArray ', () => { it('should return empty object when empty list passed in', () => { expect(libUtil.objectFromArray([], 1)).toEqual({}); }); it('should return corresponding object when empty list passed in', () => { expect( libUtil.objectFromArray([{ hello: 'world', index: 0 }], 'index') ).toEqual({ 0: { hello: 'world', index: 0 } }); }); it('should return object with undefined key when target key not found', () => { expect( libUtil.objectFromArray([{ hello: 'world', index: 0 }], 'randomKey') ).toEqual({ undefined: { hello: 'world', index: 0 } }); }); }); describe('rangedIncrement ', () => { it('should return min when sum(value, increment) < min', () => { expect(libUtil.rangedIncrement(1, 1, 3, 4)).toEqual(3); }); it('should return sum(value, increment) when min < sum(value, increment) < max', () => { expect(libUtil.rangedIncrement(2, 2, 2, 5)).toEqual(4); }); it('should return max when max < sum(value, increment)', () => { expect(libUtil.rangedIncrement(10, 10, 3, 8)).toEqual(8); }); }); describe('ctrlKey ', () => { it('should return metaKey when is mac', () => { const isMac = true; const kbEvent = new KeyboardEvent('ctrl'); expect(libUtil.ctrlKey(kbEvent)).toEqual(kbEvent.metaKey); }); it('should return ctrlKey when is not mac', () => { const isMac = false; const kbEvent = new KeyboardEvent('ctrl'); expect(libUtil.ctrlKey(kbEvent)).toEqual(kbEvent.ctrlKey); }); }); describe('getFilteredSuggestions ', () => { it('should only return valid matches to search string', async () => { const searchString = 'hello'; const getSuggestions = function() { return [ { title: 'Hello', url: 'http://www.hello.com' }, { title: 'testing saka', url: 'http://www.saka.io' } ]; }; const expectedResults = [ { title: 'Hello', url: 'http://www.hello.com', score: undefined, matches: [ { indices: [[0, 4]], value: 'Hello', key: 'title', arrayIndex: 0 }, { indices: [[0, 0], [11, 15]], value: 'http://www.hello.com', key: 'url', arrayIndex: 0 } ] } ]; const results = await libUtil.getFilteredSuggestions(searchString, { getSuggestions, threshold: 0.6, keys: ['title', 'url'] }); expect(results).toEqual(expectedResults); }); }); }); ================================================ FILE: test/options/MainOptions.test.js ================================================ import { h } from 'preact'; import { render, cleanup, wait, fireEvent, flushPromises, getByValue } from 'preact-testing-library'; import 'jest-dom/extend-expect'; import MainOptions from '@/options/Main/MainOptions.jsx'; beforeEach(() => { browser.flush(); browser.storage.sync.set.returns({}); }); test('should show all options when not showing key bindings', async () => { browser.storage.sync.get.returns({}); const { getByText, queryByText } = render(); getByText('Saka Options'); await wait(() => getByText('General Settings')); //DefaultModeSelection getByText('Default Mode'); getByText('Select the default mode Saka opens with'); //OnlyShowSearchBarSelector getByText('Suggestions on load'); getByText('Show suggestions when there is no text is the Saka search bar'); //EnableFuzzySearch getByText('Enable fuzzy search'); getByText('Enable fuzzy search for bookmarks and history search'); expect(queryByText('Keyboard Shortcuts')).not.toBeInTheDocument(); }); test('should only show key bindings when setting is true', async () => { browser.storage.sync.get.returns({}); global.SAKA_PLATFORM = 'chrome'; const { debug, getByText, getByLabelText } = render(); await wait(() => getByText('General Settings')); getByText('Saka Hotkeys'); fireEvent.click(getByText('keyboard'), { button: 0 }); await flushPromises(); getByText('Saka Options'); expect(getByText('arrow_back')); expect(getByLabelText('Back to Saka settings')).toMatchSnapshot(); expect(getByLabelText('Info about Saka custom hotkeys')).toMatchSnapshot(); getByText( 'To modify the Saka hotkeys, please visit chrome://extensions/shortcuts' ); getByText('Keyboard Shortcuts'); getByText('Open Saka'); }); test('should save settings when save button clicked', async () => { browser.storage.sync.get.returns({ sakaSettings: {} }); const { getByText, getByLabelText, getByValue } = render(); await wait(() => getByText('General Settings')); fireEvent.change(getByLabelText('Select default mode'), { target: { value: 'history' } }); await flushPromises(); fireEvent.click(getByLabelText('Suggestions on load'), { button: 0 }); await flushPromises(); fireEvent.click(getByLabelText('Enable fuzzy search'), { button: 0 }); await flushPromises(); fireEvent.click(getByValue('Save'), { button: 0 }); await flushPromises(); expect(browser.storage.sync.get.calledOnce); }); afterEach(cleanup); ================================================ FILE: test/options/__snapshots__/MainOptions.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should only show key bindings when setting is true 1`] = ` arrow_back `; exports[`should only show key bindings when setting is true 2`] = ` info `; ================================================ FILE: test/suggestion_engine/providers/bookmark.test.js ================================================ const browser = require('sinon-chrome/webextensions'); import bookmarkSuggestions from 'suggestion_engine/server/providers/bookmark.js'; describe('server/providers/bookmark ', () => { beforeAll(() => { global.browser = browser; }); beforeEach(() => { browser.flush(); }); describe('bookmarkSuggestions ', () => { it('should return all valid bookmarks when search string is empty', async () => { const settingsStore = { sakaSettings: { enableFuzzySearch: true } }; const queryResults = [ { url: 'https://google.com', title: 'Google', dateAdded: '2018-01-01' }, { url: 'https://github.com/lusakasa/saka', title: 'Saka', dateAdded: '2018-02-01' } ]; const expectedResult = [ { type: 'bookmark', score: -1, url: 'https://google.com', title: 'Google' }, { type: 'bookmark', score: -1, url: 'https://github.com/lusakasa/saka', title: 'Saka' } ]; const searchString = ''; browser.bookmarks.search.returns(queryResults); browser.storage.sync.get.returns(settingsStore); expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult); }); it('should filter all bookmarks with unknown protocol', async () => { const settingsStore = { sakaSettings: { enableFuzzySearch: true } }; const queryResults = [ { url: 'ssh://myhost.net', title: 'My Site', dateAdded: '2018-01-01' }, { url: 'https://github.com/lusakasa/saka', title: 'Saka', dateAdded: '2018-02-01' } ]; const expectedResult = [ { type: 'bookmark', score: -1, url: 'https://github.com/lusakasa/saka', title: 'Saka' } ]; const searchString = ''; browser.bookmarks.search.returns(queryResults); browser.storage.sync.get.returns(settingsStore); expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult); }); it('should filter all bookmarks with invalid URL', async () => { const settingsStore = { sakaSettings: { enableFuzzySearch: true } }; const queryResults = [ { url: 'myhostnet', title: 'My Site', dateAdded: '2018-01-01' }, { url: 'https://github.com/lusakasa/saka', title: 'Saka', dateAdded: '2018-02-01' } ]; const expectedResult = [ { type: 'bookmark', score: -1, url: 'https://github.com/lusakasa/saka', title: 'Saka' } ]; const searchString = ''; browser.bookmarks.search.returns(queryResults); browser.storage.sync.get.returns(settingsStore); expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult); }); it('should return fuzzy search matching results', async () => { const settingsStore = { sakaSettings: { enableFuzzySearch: true } }; const queryResults = [ { url: 'myhostnet', title: 'My Site', dateAdded: '2018-01-01' }, { url: 'https://github.com/lusakasa/saka', title: 'Saka', dateAdded: '2018-02-01' } ]; const expectedResult = [ { type: 'bookmark', url: 'https://github.com/lusakasa/saka', title: 'Saka', score: undefined, matches: [ { indices: [[0, 3]], value: 'Saka', key: 'title', arrayIndex: 0 }, { indices: [[4, 4], [21, 24]], value: 'https://github.com/lusakasa/saka', key: 'url', arrayIndex: 0 } ] } ]; const searchString = 'Saka'; browser.bookmarks.search.returns(queryResults); browser.storage.sync.get.returns(settingsStore); expect(await bookmarkSuggestions(searchString)).toEqual(expectedResult); }); }); afterAll(() => { browser.flush(); delete global.browser; }); }); ================================================ FILE: test/suggestion_engine/providers/closedTab.test.js ================================================ const browser = require('sinon-chrome/webextensions'); import closedTabSuggestions, { getAllSuggestions } from 'suggestion_engine/server/providers/closedTab.js'; describe('server/providers/closedTabs ', () => { beforeAll(() => { global.browser = browser; }); beforeEach(() => { browser.flush(); }); describe('closedTabSuggestions ', () => { it('should return all closed tabs when no search string provided', async () => { const queryResults = [ { lastModified: 123456, tab: { id: 1, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false } } ]; const expectedResult = [ { type: 'closedTab', tabId: 1, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', sessionId: undefined, score: undefined, incognito: false, lastAccessed: 123456 } ]; const searchString = ''; const sakaId = 'abcdefg/saka.html'; browser.runtime.getURL.returns(sakaId); browser.sessions.getRecentlyClosed.returns(queryResults); expect(await closedTabSuggestions(searchString)).toEqual(expectedResult); }); it('should filter out entries for saka in recently closed tabs', async () => { const queryResults = [ { lastModified: 123456, tab: { id: 1, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: true } }, { lastModified: 654321, tab: { id: 2, windowId: 0, title: 'Saka Extension', url: 'chrome-extension://abcdefg/saka.html', favIconUrl: '', incognito: false } } ]; const expectedResult = [ { type: 'closedTab', tabId: 1, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: null, sessionId: undefined, score: undefined, incognito: true, lastAccessed: 123456 } ]; const searchString = ''; const sakaId = 'abcdefg/saka.html'; browser.runtime.getURL.returns(sakaId); browser.sessions.getRecentlyClosed.returns(queryResults); expect(await closedTabSuggestions(searchString)).toEqual(expectedResult); }); it('should return all closed tabs matching searchString', async () => { const queryResults = [ { lastModified: 123456, tab: { id: 1, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false } }, { lastModified: 654321, tab: { id: 2, windowId: 0, title: 'Google', url: 'https://google.com', favIconUrl: 'https://google.com/icon.png', incognito: true } } ]; const expectedResult = [ { type: 'closedTab', tabId: 2, title: 'Google', url: 'https://google.com', favIconUrl: null, sessionId: undefined, score: undefined, incognito: true, lastAccessed: 654321, matches: [ { indices: [[0, 3]], value: 'Google', key: 'title', arrayIndex: 0 }, { indices: [[8, 11]], value: 'https://google.com', key: 'url', arrayIndex: 0 } ] } ]; const searchString = 'Goog'; const sakaId = 'abcdefg/saka.html'; browser.runtime.getURL.returns(sakaId); browser.sessions.getRecentlyClosed.returns(queryResults); expect(await closedTabSuggestions(searchString)).toEqual(expectedResult); }); }); describe('getAllSuggestions', () => { it('should work for window sessions', async () => { const queryResults = [ { window: { lastModified: 123456, tabs: [ { id: 1, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false } ] } } ]; const expectedResult = [ { type: 'closedTab', tabId: 1, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', sessionId: undefined, score: undefined, incognito: false, lastAccessed: 123456 } ]; const sakaId = 'abcdefg/saka.html'; browser.runtime.getURL.returns(sakaId); browser.sessions.getRecentlyClosed.returns(queryResults); expect(await getAllSuggestions()).toEqual(expectedResult); }); }); afterAll(() => { browser.flush(); delete global.browser; }); }); ================================================ FILE: test/suggestion_engine/providers/history.test.js ================================================ const browser = require('sinon-chrome/webextensions'); import getHistorySuggestions from 'suggestion_engine/server/providers/history.js'; describe('server/providers/history ', () => { beforeAll(() => { global.browser = browser; }); beforeEach(() => { browser.flush(); }); describe('getHistorySuggestions ', () => { it('should not use fuzzy search when enableFuzzySearch setting is set to false', async () => { const settingsStore = { sakaSettings: { enableFuzzySearch: false } }; const queryResults = [ { id: 1, url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastVisitTime: 1524795334, visitCount: 5, typedCount: 10 }, { id: 2, url: 'https://example.com', title: 'Example', lastVisitTime: 1524794200, visitCount: 1, typedCount: 0 } ]; const expectedResult = [ { type: 'history', url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastAccessed: 1524795.334, score: 15 }, { type: 'history', title: 'Example', url: 'https://example.com', lastAccessed: 1524794.2, score: 1 } ]; const searchString = 'Saka'; const sakaURL = 'nbdfpcokndmap/saka.html'; browser.runtime.getURL.returns(sakaURL); browser.history.search.returns(queryResults); browser.storage.sync.get.returns(settingsStore); expect(await getHistorySuggestions(searchString)).toEqual(expectedResult); }); it('should use fuzzy search when enableFuzzySearch setting is set to true', async () => { const settingsStore = { sakaSettings: { enableFuzzySearch: true } }; const queryResults = [ { id: 1, url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastVisitTime: 1524795334, visitCount: 5, typedCount: 10 }, { id: 2, url: 'https://example.com', title: 'Example', lastVisitTime: 1524794200, visitCount: 1, typedCount: 0 } ]; const expectedResult = [ { type: 'history', url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastAccessed: 1524795.334, score: undefined, matches: [ { indices: [[0, 3]], value: 'Saka Github', key: 'title', arrayIndex: 0 }, { indices: [[4, 4], [21, 24]], value: 'https://github.com/lusakasa/saka', key: 'url', arrayIndex: 0 } ] }, { type: 'history', title: 'Example', url: 'https://example.com', lastAccessed: 1524794.2, score: undefined, matches: [ { indices: [[2, 2]], value: 'Example', key: 'title', arrayIndex: 0 }, { indices: [[4, 4], [10, 10]], value: 'https://example.com', key: 'url', arrayIndex: 0 } ] } ]; const searchString = 'Saka'; const sakaURL = 'nbdfpcokndmap/saka.html'; browser.runtime.getURL.returns(sakaURL); browser.history.search.returns(queryResults); browser.storage.sync.get.returns(settingsStore); expect(await getHistorySuggestions(searchString)).toEqual(expectedResult); }); it('should return all visited site history except Saka Options URL', async () => { const settingsStore = { sakaSettings: { enableFuzzySearch: true } }; const queryResults = [ { id: 1, url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastVisitTime: 1524795334, visitCount: 5, typedCount: 10 }, { id: 2, url: 'chrome://nbdfpcokndmap/options.html', title: 'Options', lastVisitTime: 1524794200, visitCount: 1, typedCount: 0 } ]; const expectedResult = [ { type: 'history', url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastAccessed: 1524795.334, score: 15 } ]; const searchString = ''; const sakaURL = 'nbdfpcokndmap/saka.html'; browser.runtime.getURL.returns(sakaURL); browser.history.search.returns(queryResults); browser.storage.sync.get.returns(settingsStore); expect(await getHistorySuggestions(searchString)).toEqual(expectedResult); }); }); afterAll(() => { browser.flush(); delete global.browser; }); }); ================================================ FILE: test/suggestion_engine/providers/mode.test.js ================================================ import modeSuggestions from 'suggestion_engine/server/providers/mode.js'; import { ctrlChar } from 'lib/utils'; import { colorMap, fadedColorMap } from 'lib/colors'; describe('server/providers/mode ', () => { describe('modeSuggestions ', () => { it('should return all modes when no search string provided', async () => { const expectedResult = [ { type: 'mode', mode: 'tab', label: 'Tabs', shortcut: `${ctrlChar}-shift-a`, color: colorMap.tab, fadedColor: fadedColorMap.tab, icon: 'tab' }, { type: 'mode', mode: 'closedTab', label: 'Recently Closed Tabs', shortcut: `${ctrlChar}-shift-c`, color: colorMap.closedTab, fadedColor: fadedColorMap.closedTab, icon: 'restore_page' }, { type: 'mode', label: 'Bookmarks', mode: 'bookmark', shortcut: `${ctrlChar}-b`, color: colorMap.bookmark, fadedColor: fadedColorMap.bookmark, icon: 'bookmark_border' }, { type: 'mode', label: 'History', mode: 'history', shortcut: `${ctrlChar}-shift-e`, color: colorMap.history, fadedColor: fadedColorMap.history, icon: 'history' }, { type: 'mode', label: 'Recently Viewed', mode: 'recentlyViewed', shortcut: `${ctrlChar}-shift-x`, color: colorMap.recentlyViewed, fadedColor: fadedColorMap.recentlyViewed, icon: 'timelapse' } ]; const searchString = ''; expect(await modeSuggestions(searchString)).toEqual(expectedResult); }); it('should return closedTab search mode when search string `cl` provided', async () => { const expectedResult = [ { type: 'mode', mode: 'closedTab', label: 'Recently Closed Tabs', shortcut: `${ctrlChar}-shift-c`, color: 'rgba(0,0,0,1)', fadedColor: 'rgba(0,0,0,0.44)', icon: 'restore_page', score: undefined, matches: [ { indices: [[2, 2], [6, 6], [9, 10]], value: 'Recently Closed Tabs', key: 'label', arrayIndex: 0 } ] }, { type: 'mode', label: 'Recently Viewed', mode: 'recentlyViewed', shortcut: `${ctrlChar}-shift-x`, color: 'rgba(152,78,163,1)', fadedColor: 'rgba(152,78,163,0.44)', icon: 'timelapse', score: undefined, matches: [ { indices: [[2, 2], [6, 6]], value: 'Recently Viewed', key: 'label', arrayIndex: 0 } ] } ]; const searchString = 'cl'; expect(await modeSuggestions(searchString)).toEqual(expectedResult); }); }); }); ================================================ FILE: test/suggestion_engine/providers/recentlyViewed.test.js ================================================ const browser = require('sinon-chrome/webextensions'); import recentlyViewedSuggestions from 'suggestion_engine/server/providers/recentlyViewed.js'; describe('server/providers/recentlyViewed ', () => { beforeAll(() => { global.browser = browser; }); beforeEach(() => { browser.flush(); // Clears the database and adds some testing data. // Jest will wait for this promise to resolve before running tests. global.SAKA_PLATFORM = 'chrome'; }); describe('recentlyViewedSuggestions ', () => { it('should return all recently viewed tabs when search string is empty', async () => { const trackedHistory = { tabHistory: [ { tabId: 1, lastAccessed: 123456 }, { tabId: 0, lastAccessed: 654321 } ], recentlyClosed: [ { tab: { tabId: 1, lastAccessed: 111111 } }, { tab: { tabId: 4, lastAccessed: 222222 } } ] }; const tabResults = [ { id: 1, windowId: 0, title: 'Google', url: 'https://google.com', favIconUrl: 'https://google.com/icon.png', incognito: false, lastAccessed: 123456 }, { id: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: true, lastAccessed: 654321 } ]; const recentlyClosedResults = [ { lastModified: 123456, tab: { id: 1, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false, sessionId: 'abc123' } }, { lastModified: 19191, tab: { id: 2, windowId: 0, title: 'Recently Viewed Mode', url: 'https://github.com/lusakasa/saka/pull/45', favIconUrl: 'https://github.com/icon.png', incognito: true, sessionId: '123abc' } } ]; const historyResults = [ { id: 1, url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastVisitTime: 1524795334, visitCount: 5, typedCount: 10 }, { id: 3, url: 'https://example.com', title: 'Example', lastVisitTime: 1524794200, visitCount: 1, typedCount: 0 } ]; const expectedResult = [ { type: 'recentlyViewed', url: 'https://example.com', title: 'Example', lastAccessed: 1524794.2, score: 1, originalType: 'history' }, { lastAccessed: 19191, tabId: 2, title: 'Recently Viewed Mode', url: 'https://github.com/lusakasa/saka/pull/45', favIconUrl: null, incognito: true, sessionId: '123abc', score: undefined, type: 'recentlyViewed', originalType: 'closedTab' }, { type: 'recentlyViewed', tabId: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: null, incognito: true, lastAccessed: 654.321, originalType: 'tab' }, { type: 'recentlyViewed', tabId: 1, windowId: 0, title: 'Google', url: 'https://google.com', favIconUrl: 'https://google.com/icon.png', incognito: false, lastAccessed: 123.456, originalType: 'tab' } ]; const searchString = ''; const sakaId = 'abcdefg/saka.html'; browser.tabs.query.returns(tabResults); browser.runtime.getURL.returns(sakaId); browser.runtime.getBackgroundPage.returns(trackedHistory); browser.sessions.getRecentlyClosed.returns(recentlyClosedResults); browser.history.search.returns(historyResults); expect(await recentlyViewedSuggestions(searchString)).toEqual( expectedResult ); }); it('should return matching recently viewed tabs when search string is not empty', async () => { const tabHistory = { tabHistory: [0] }; const tabResults = [ { id: 1, windowId: 0, title: 'Google', url: 'https://google.com', favIconUrl: 'https://google.com/icon.png', incognito: false, lastAccessed: 123456 }, { id: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: true, lastAccessed: 654321 } ]; const recentlyClosedResults = [ { lastModified: 123456, tab: { id: 1, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false, sessionId: 'abc123' } }, { lastModified: 19191, tab: { id: 2, windowId: 0, title: 'Recently Viewed Mode', url: 'https://github.com/lusakasa/saka/pull/45', favIconUrl: 'https://github.com/icon.png', incognito: true, sessionId: '123abc' } } ]; const historyResults = [ { id: 1, url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastVisitTime: 1524795334, visitCount: 5, typedCount: 10 }, { id: 3, url: 'https://example.com', title: 'Example', lastVisitTime: 1524794200, visitCount: 1, typedCount: 0 } ]; const expectedResult = [ { type: 'recentlyViewed', tabId: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: null, incognito: true, lastAccessed: 654.321, originalType: 'tab', score: undefined, matches: [ Object({ indices: [[0, 3]], value: 'Saka', key: 'title', arrayIndex: 0 }), Object({ indices: [[4, 4], [21, 24]], value: 'https://github.com/lusakasa/saka', key: 'url', arrayIndex: 0 }) ] }, { type: 'recentlyViewed', url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastAccessed: 1524795.334, score: undefined, originalType: 'history', matches: [ Object({ indices: [[0, 3]], value: 'Saka Github', key: 'title', arrayIndex: 0 }), Object({ indices: [[4, 4], [21, 24]], value: 'https://github.com/lusakasa/saka', key: 'url', arrayIndex: 0 }) ] }, { type: 'recentlyViewed', tabId: 2, title: 'Recently Viewed Mode', url: 'https://github.com/lusakasa/saka/pull/45', favIconUrl: null, incognito: true, lastAccessed: 19191, sessionId: '123abc', score: undefined, originalType: 'closedTab', matches: [ Object({ indices: [[4, 4], [21, 24]], value: 'https://github.com/lusakasa/saka/pull/45', key: 'url', arrayIndex: 0 }) ] } ]; const searchString = 'saka'; const sakaId = 'abcdefg/saka.html'; browser.tabs.query.returns(tabResults); browser.runtime.getURL.returns(sakaId); browser.runtime.getBackgroundPage.returns(tabHistory); browser.sessions.getRecentlyClosed.returns(recentlyClosedResults); browser.history.search.returns(historyResults); expect(await recentlyViewedSuggestions(searchString)).toEqual( expectedResult ); }); }); afterAll(() => { browser.flush(); delete global.browser; }); }); ================================================ FILE: test/suggestion_engine/providers/tab.test.js ================================================ const browser = require('sinon-chrome/webextensions'); import tabSuggestions from 'suggestion_engine/server/providers/tab.js'; describe('server/providers/tab ', () => { beforeAll(() => { global.browser = browser; }); beforeEach(() => { browser.flush(); }); describe('tabSuggestions ', () => { it('should return all recent tabs when search string is empty', async () => { const queryResults = [ { id: 1, windowId: 0, title: 'Google', url: 'https://google.com', favIconUrl: 'https://google.com/icon.png', incognito: false, lastAccessed: 123456 }, { id: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: true, lastAccessed: 654321 } ]; const tabHistory = { tabHistory: [ { tabId: 1, lastAccessed: 123456 }, { tabId: 0, lastAccessed: 654321 } ] }; const expectedResult = [ { type: 'tab', tabId: 1, windowId: 0, title: 'Google', url: 'https://google.com', favIconUrl: 'https://google.com/icon.png', incognito: false, lastAccessed: 123456 }, { type: 'tab', tabId: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: null, incognito: true, lastAccessed: 654321 } ]; const searchString = ''; browser.tabs.query.returns(queryResults); browser.runtime.getBackgroundPage.returns(tabHistory); expect(await tabSuggestions(searchString)).toEqual(expectedResult); }); it('should return all tabs matching searchString', async () => { const queryResults = [ { id: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false, lastAccessed: 123456 }, { id: 0, windowId: 0, title: 'Google', url: 'https://google.com', favIconUrl: 'https://google.com/icon.png', incognito: false, lastAccessed: 654321 } ]; const expectedResult = [ { type: 'tab', tabId: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false, lastAccessed: 123.456, matches: [ { indices: [[0, 3]], value: 'Saka', key: 'title', arrayIndex: 0 }, { indices: [[4, 4], [21, 24]], value: 'https://github.com/lusakasa/saka', key: 'url', arrayIndex: 0 } ], score: undefined } ]; const searchString = 'saka'; browser.tabs.query.returns(queryResults); expect(await tabSuggestions(searchString)).toEqual(expectedResult); }); }); afterAll(() => { browser.flush(); delete global.browser; }); }); ================================================ FILE: test/suggestion_engine/server/index.test.js ================================================ import { getSuggestions, activateSuggestion, closeTab } from 'suggestion_engine/server/index.js'; import * as providers from 'suggestion_engine/server/providers/index.js'; jest.mock('suggestion_engine/server/providers/index.js', () => ({ tab: jest.fn().mockImplementation(() => [ { type: 'tab' } ]), closedTab: jest.fn(), mode: jest.fn(), history: jest.fn(), bookmark: jest.fn(), recentlyViewed: jest.fn() })); test('should show all options when not showing key bindings', async () => { const mockexpectedResult = [ { type: 'tab' } ]; const suggestions = await getSuggestions(['tab', 'saka']); expect(suggestions).toEqual(mockexpectedResult); }); test('should call appropriate activation methods', async () => { browser.flush(); await activateSuggestion({ type: 'tab' }); expect(browser.tabs.update.calledOnce).toEqual(true); expect(browser.windows.update.calledOnce).toEqual(true); browser.flush(); await activateSuggestion({ type: 'closedTab' }); expect(browser.sessions.restore.calledOnce).toEqual(true); browser.flush(); await activateSuggestion({ type: 'bookmark' }); expect(browser.tabs.create.calledOnce).toEqual(true); browser.flush(); await activateSuggestion({ type: 'history' }); expect(browser.tabs.create.calledOnce).toEqual(true); browser.flush(); await activateSuggestion({ type: 'recentlyViewed', originalType: 'tab' }); expect(browser.tabs.update.calledOnce).toEqual(true); }); test('should focus bookmark and history tabs if already open', async () => { browser.flush(); browser.tabs.query.resolves([{ id: '1', windowId: '1' }]); await activateSuggestion({ type: 'bookmark' }); expect(browser.tabs.update.calledOnce).toEqual(true); expect(browser.windows.update.calledOnce).toEqual(true); browser.flush(); browser.tabs.query.resolves([{ id: '1', windowId: '1' }]); await activateSuggestion({ type: 'history' }); expect(browser.tabs.update.calledOnce).toEqual(true); expect(browser.windows.update.calledOnce).toEqual(true); }); test('should call close tab API', async () => { const suggestion = { tabId: 1 }; await closeTab(suggestion); }); ================================================ FILE: test/suggestion_utils/index.test.js ================================================ import { preprocessSuggestion } from '@/suggestion_utils/index.js'; import { colorMap, fadedColorMap } from 'lib/colors'; import * as url from 'lib/url.js'; test('should return suggestion with pretty URL when type is tab', () => { url.prettifyURL = jest .fn() .mockImplementation(() => 'https://github.com/lusakasa/saka'); const suggestion = { type: 'tab', tabId: 0, windowId: 0, title: 'Saka', url: 'https://github.com/lusakasa/saka?stuffInUrl=true', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', incognito: false, lastAccessed: 123.456, matches: [], score: undefined }; const searchText = 'saka'; const result = preprocessSuggestion(suggestion, searchText); expect(result).toEqual({ ...suggestion, prettyURL: 'https://github.com/lusakasa/saka', text: suggestion.title }); }); test('should return suggestion with pretty URL when type is closedTab', () => { url.prettifyURL = jest .fn() .mockImplementation(() => 'https://github.com/lusakasa/saka'); const suggestion = { type: 'closedTab', tabId: 1, title: 'Saka', url: 'https://github.com/lusakasa/saka?stuffInUrl=true', favIconUrl: 'https://github.com/lusakasa/saka/icon.png', sessionId: undefined, score: undefined, incognito: false, lastAccessed: 123456 }; const searchText = 'saka'; const result = preprocessSuggestion(suggestion, searchText); expect(result).toEqual({ ...suggestion, prettyURL: 'https://github.com/lusakasa/saka', text: suggestion.title }); }); test('should return suggestion when type is mode', () => { url.prettifyURL = jest.fn().mockImplementation(suggestion => suggestion); const suggestion = { type: 'mode', mode: 'tab', label: 'Tabs', shortcut: `ctrl-shift-a`, color: colorMap.tab, fadedColor: fadedColorMap.tab, icon: 'tab' }; const searchText = 'saka'; const result = preprocessSuggestion(suggestion, searchText); expect(result).toEqual(suggestion); }); test('should return suggestion with pretty URL when type is bookmark', () => { url.prettifyURL = jest .fn() .mockImplementation(() => 'https://github.com/lusakasa/saka'); const suggestion = { type: 'bookmark', score: -1, url: 'https://github.com/lusakasa/saka', title: 'Saka' }; const searchText = 'saka'; const result = preprocessSuggestion(suggestion, searchText); expect(result).toEqual({ ...suggestion, prettyURL: 'https://github.com/lusakasa/saka', text: 'https://github.com/lusakasa/saka' }); }); test('should return suggestion with pretty URL when type is history', () => { url.prettifyURL = jest .fn() .mockImplementation(() => 'https://github.com/lusakasa/saka'); const suggestion = { type: 'history', url: 'https://github.com/lusakasa/saka', title: 'Saka Github', lastAccessed: 1524795.334, score: 15 }; const searchText = 'saka'; const result = preprocessSuggestion(suggestion, searchText); expect(result).toEqual({ ...suggestion, prettyURL: 'https://github.com/lusakasa/saka', text: 'https://github.com/lusakasa/saka' }); }); test('should return suggestion with pretty URL when type is recentlyViewed', () => { url.prettifyURL = jest .fn() .mockImplementation(() => 'https://github.com/lusakasa/saka'); const suggestion = { type: 'recentlyViewed', url: 'https://example.com', title: 'Example', lastAccessed: 1524794.2, score: 1, originalType: 'history' }; const searchText = 'saka'; const result = preprocessSuggestion(suggestion, searchText); expect(result).toEqual({ ...suggestion, prettyURL: 'https://github.com/lusakasa/saka', text: 'https://github.com/lusakasa/saka' }); }); test('should return error when type is unexpected', () => { const suggestion = { type: 'adsknajksfnasjfnjas', url: 'https://example.com', title: 'Example', lastAccessed: 1524794.2, score: 1, originalType: 'history' }; const searchText = 'saka'; const result = preprocessSuggestion(suggestion, searchText); expect(result).toEqual({ type: 'error', title: `Error. Unknown Suggestion type: ${suggestion.type}`, text: `Error. Unknown Suggestion type: ${suggestion.type}` }); }); ================================================ FILE: webpack.config.js ================================================ const webpack = require('webpack'); const BabiliPlugin = require('babili-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const GenerateJsonPlugin = require('generate-json-webpack-plugin'); const marked = require('marked'); const path = require('path'); const merge = require('webpack-merge'); const { version } = require('./manifest/common.json'); // mode controls: const renderer = new marked.Renderer(); // process.traceDeprecation = true; // markdown convert to html module.exports = function webpackConfig(env) { const [mode, platform, benchmark] = env.split(':'); const config = { resolve: { alias: { react: 'preact-compat', 'react-dom': 'preact-compat', src: path.join(__dirname, 'src'), msg: path.join(__dirname, 'src/msg'), suggestion_engine: path.join(__dirname, 'src/suggestion_engine'), suggestion_utils: path.join(__dirname, 'src/suggestion_utils'), lib: path.join(__dirname, 'src/lib'), scss: path.join(__dirname, 'src/scss') }, modules: ['./src', './node_modules'] }, entry: { background_page: 'src/background_page/index.js', toggle_saka: 'src/content_script/toggle_saka.js', // 'extensions': './src/pages/extensions/index.js', // 'info': './src/pages/info/index.js', // 'options': './src/pages/options/index.js', saka: 'src/saka/index.jsx', 'saka-options': 'src/options/saka-options.jsx' }, optimization: { splitChunks: { cacheGroups: { commons: { test: /[\\/]node_modules[\\/]/, name: 'vendor', chunks: 'all' } } } }, output: { path: `${__dirname}/dist`, filename: '[name].js' // sourceMapFilename: '[name].js.map' }, module: { rules: [ { test: /\.(jsx|js)$/, exclude: /node_modules/, loaders: ['babel-loader'] }, { test: /\.(sc|c)ss$/, loaders: [ 'style-loader', 'css-loader', { loader: 'sass-loader', options: { importer(url, prev) { if (url.indexOf('@material') === 0) { const filePath = url.split('@material')[1]; const nodeModulePath = `./node_modules/@material/${filePath}`; return { file: path.resolve(nodeModulePath) }; } return { file: url }; } } } ] }, { test: /\.md$/, loaders: [ 'html-loader', { loader: 'markdown-loader', options: { renderer } } ] } ] }, plugins: [ new webpack.optimize.ModuleConcatenationPlugin(), new CopyWebpackPlugin([ { from: 'static' }, { context: 'src/modes', from: '**/default.json', to: 'default_[folder].json' }, { context: 'src/modes', from: '**/config.json', to: 'config_[folder].json' } ]), new GenerateJsonPlugin( 'manifest.json', merge( require('./manifest/common.json'), require(`./manifest/${platform}.json`), { version } ), null, 2 ) ] }; // 1. SAKA_DEBUG: boolean(true | false) // * true for development builds // * false for production build // If you want something to run only in testing/development, use // if (SAKA_DEBUG) { console.log(variable); }. // All code within will be removed at build time in production builds. // platform controls: // 1. SAKA_PLATFORM: string('chrome' | 'firefox' | 'edge') // Use this to provide platform specific features, e.g. use shadow DOM // on chrome but css selectors on firefox and edge for link hint styling if (mode === 'prod') { config.plugins = config.plugins.concat([ new BabiliPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), SAKA_DEBUG: JSON.stringify(false), SAKA_VERSION: JSON.stringify(version), SAKA_PLATFORM: JSON.stringify(platform), SAKA_BENCHMARK: JSON.stringify(true) }) ]); } else { config.devtool = 'source-map'; config.plugins = config.plugins.concat([ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('development'), SAKA_DEBUG: JSON.stringify(true), SAKA_VERSION: JSON.stringify(`${version} dev`), SAKA_PLATFORM: JSON.stringify(platform), SAKA_BENCHMARK: JSON.stringify(benchmark === 'benchmark') }) ]); } return config; };