├── .eslintignore ├── screenshots ├── popup.png └── add-worklog-sample.gif ├── jest.config.integration.json ├── jest.config.json ├── chrome-extension ├── img │ ├── spinner-small.gif │ ├── ic_error_black_24dp.png │ ├── open_in_new_window.png │ ├── ic_delete_black_24dp.png │ ├── ic_cloud_done_black_24dp.png │ ├── icon-time-task-512x512.png │ ├── ic_cloud_upload_black_24dp.png │ └── ic_cloud_upload_black_48dp.png ├── js │ ├── open-popup.js │ ├── content.js │ ├── gtm-head.js │ ├── popup.js │ ├── gtm-tags.js │ ├── update-script.js │ ├── jira-parser.js │ ├── options.js │ ├── model.js │ ├── controller.js │ ├── jira-helper.js │ └── view.js ├── temp-popup.html ├── manifest.json ├── options.html ├── lib │ ├── mediator.min.js │ └── axios.min.js ├── styles.css └── popup.html ├── jest.config.unit.json ├── .editorconfig ├── .vscode ├── settings.json └── launch.json ├── tests ├── unit │ ├── get-rest-issue.json │ ├── update-script.unit.spec.js │ ├── jira-helper.unit.spec.js │ └── jira-parser.unit.spec.js └── integration │ ├── browser-manager.int.spec.js │ ├── browser-manager.js │ ├── jira-mock.js │ ├── test-puppeteer.js │ ├── uitest.int.spec.js │ └── jira-extension-functions.js ├── CHANGELOG.md ├── .eslintrc.json ├── scripts ├── sync-version.js ├── package.js ├── build-for-test.js └── deploy.js ├── LICENSE.md ├── .gitignore ├── .circleci └── config.yml ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | chrome-extension/lib 2 | chrome-extension/js/gtm-head.js -------------------------------------------------------------------------------- /screenshots/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/screenshots/popup.png -------------------------------------------------------------------------------- /jest.config.integration.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "jsdom", 3 | "testRegex": "int.spec.js" 4 | } -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "jsdom", 3 | "reporters": [ "default", "jest-junit" ] 4 | } -------------------------------------------------------------------------------- /screenshots/add-worklog-sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/screenshots/add-worklog-sample.gif -------------------------------------------------------------------------------- /chrome-extension/img/spinner-small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/spinner-small.gif -------------------------------------------------------------------------------- /chrome-extension/img/ic_error_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/ic_error_black_24dp.png -------------------------------------------------------------------------------- /chrome-extension/img/open_in_new_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/open_in_new_window.png -------------------------------------------------------------------------------- /chrome-extension/img/ic_delete_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/ic_delete_black_24dp.png -------------------------------------------------------------------------------- /jest.config.unit.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "jsdom", 3 | "testRegex": "unit.spec.js", 4 | "reporters": [ "default", "jest-junit" ] 5 | } -------------------------------------------------------------------------------- /chrome-extension/img/ic_cloud_done_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/ic_cloud_done_black_24dp.png -------------------------------------------------------------------------------- /chrome-extension/img/icon-time-task-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/icon-time-task-512x512.png -------------------------------------------------------------------------------- /chrome-extension/img/ic_cloud_upload_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/ic_cloud_upload_black_24dp.png -------------------------------------------------------------------------------- /chrome-extension/img/ic_cloud_upload_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfeugds/jiraworklogtool/HEAD/chrome-extension/img/ic_cloud_upload_black_48dp.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true, 3 | "eslint.run":"onSave", 4 | "eslint.trace.server": "messages", 5 | "eslint.options": { 6 | "configFile": ".eslintrc.json" 7 | }, 8 | } -------------------------------------------------------------------------------- /chrome-extension/js/open-popup.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | var popupWindow = window.open( 3 | chrome.runtime.getURL('popup.html'), 4 | 'Jira Worklog Tool', 5 | 'width=610,height=500' 6 | ) 7 | popupWindow.focus() 8 | window.close() 9 | -------------------------------------------------------------------------------- /chrome-extension/js/content.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | const { id } = chrome.runtime 3 | console.log('content script!', { id }) 4 | 5 | const idElem = document.createElement('input') 6 | idElem.setAttribute('id', 'jiraworklog_id') 7 | idElem.setAttribute('value', id) 8 | idElem.setAttribute('type', 'hidden') 9 | 10 | document.body.appendChild(idElem) 11 | -------------------------------------------------------------------------------- /tests/unit/get-rest-issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "worklogs": [ 3 | { 4 | "author": { 5 | "key": "hue@br.com" 6 | }, 7 | "comment": "tech onboarding", 8 | "started": "2018-03-26T06:00:00.000+0000", 9 | "timeSpent": "1h 50m", 10 | "id": "55829" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG.md 2 | 3 | ## 0.4.5 (unreleased) 4 | 5 | Infra: 6 | 7 | - running UI tests in CircleCI 8 | - getting extension ID dynamically to run UI tests 9 | - changing build strategy for extension, generate manifest dynamically for UI tests 10 | - add test result in circleci with jest junit 11 | 12 | Tests: 13 | 14 | - improved UI tests consistency 15 | - refactor UI test spec to reuse functions -------------------------------------------------------------------------------- /chrome-extension/js/gtm-head.js: -------------------------------------------------------------------------------- 1 | (function (w, d, s, l, i) { 2 | w[l] = w[l] || []; w[l].push({ 'gtm.start': 3 | new Date().getTime(), 4 | event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0] 5 | var j = d.createElement(s); var dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = 6 | 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f) 7 | })(window, document, 'script', 'dataLayer', 'GTM-MZ6T8MS') 8 | -------------------------------------------------------------------------------- /chrome-extension/temp-popup.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | Jira Worklog Helper 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/tests/integration/test-puppeteer.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "standard" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly", 13 | "axios": true, 14 | "fail": false 15 | }, 16 | "parserOptions": { 17 | "ecmaVersion": 2018 18 | }, 19 | "rules": { 20 | "no-useless-escape": "warn", 21 | "indent": ["error", 2] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/sync-version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const semver = require('semver'); 3 | 4 | // Write package.json's versino to the src/manifest.json 5 | const packageJson = require('../package'); 6 | 7 | const validVersion = semver.valid(packageJson.version); 8 | console.log(validVersion); 9 | 10 | const manifestLocation = '../chrome-extension/manifest'; 11 | const manifest = require(manifestLocation); 12 | manifest.version = validVersion; 13 | 14 | fs.writeFileSync('chrome-extension/manifest.json', JSON.stringify(manifest, null, ' ')); -------------------------------------------------------------------------------- /tests/integration/browser-manager.int.spec.js: -------------------------------------------------------------------------------- 1 | const browserManager = require('./browser-manager') 2 | 3 | describe('Browser Manager', () => { 4 | test('Get browser instance with extension installed', async () => { 5 | try { 6 | const browser = await browserManager.getBrowser() 7 | const { popupUrl } = await browserManager.getExtensionInfo(browser) 8 | 9 | expect(browser.newPage).not.toBeNull() 10 | expect(popupUrl).toMatch(/chrome-extension:\/\/\w{32}\/popup.html/) 11 | 12 | await browser.close() 13 | } catch (e) { 14 | fail(e) 15 | } 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /chrome-extension/js/popup.js: -------------------------------------------------------------------------------- 1 | /* global updateScript */ 2 | var Mediator = window.Mediator || {} 3 | var View = window.View || {} 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | // TODO: refactor debug log 7 | var DEBUG = true 8 | if (!DEBUG) { 9 | if (!window.console) window.console = {} 10 | var methods = ['log', 'debug', 'warn', 'info'] 11 | for (var i = 0; i < methods.length; i++) { 12 | console[methods[i]] = function () {} 13 | } 14 | } 15 | 16 | window.mediator = new Mediator() 17 | 18 | // palliative solution for storage sync issue 19 | updateScript.run().then(() => { 20 | View.Main.init() 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Jira Worklog Tool", 4 | "version": "0.5.1", 5 | "author": "alfeugds", 6 | "description": "This extension allows the user to log the work in Jira easily.", 7 | "homepage_url": "https://github.com/alfeugds/jiraworklogtool", 8 | "icons": { 9 | "128": "img/icon-time-task-512x512.png" 10 | }, 11 | "options_page": "options.html", 12 | "action": { 13 | "default_icon": "img/icon-time-task-512x512.png", 14 | "default_popup": "temp-popup.html", 15 | "default_title": "Log Work" 16 | }, 17 | "permissions": [ 18 | "storage" 19 | ], 20 | "host_permissions": [ 21 | "*://*/*" 22 | ], 23 | "content_security_policy": { 24 | "extension_pages": "script-src 'self'; object-src 'self'" 25 | } 26 | } -------------------------------------------------------------------------------- /scripts/package.js: -------------------------------------------------------------------------------- 1 | // const AdmZip = require('adm-zip'); 2 | 3 | // // Archive the extension folder into 'chrome-extension.zip' 4 | // const zip = new AdmZip(); 5 | // zip.addLocalFolder('chrome-extension'); 6 | // zip.writeZip('chrome-extension.zip'); 7 | 8 | var file_system = require('fs'); 9 | var archiver = require('archiver'); 10 | 11 | var output = file_system.createWriteStream('chrome-extension.zip'); 12 | var archive = archiver('zip'); 13 | 14 | output.on('close', function () { 15 | console.log(archive.pointer() + ' total bytes'); 16 | console.log('archiver has been finalized and the output file descriptor has closed.'); 17 | }); 18 | 19 | archive.on('error', function(err){ 20 | throw err; 21 | }); 22 | 23 | archive.pipe(output); 24 | archive.glob('**/*', { cwd: 'chrome-extension' }); 25 | archive.finalize(); -------------------------------------------------------------------------------- /scripts/build-for-test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const fs = require('fs-extra') 3 | const path = require('path'); 4 | 5 | const SRC_DIR = path.join(process.cwd(), 'chrome-extension') 6 | const DIST_DIR = path.join(process.cwd(), 'dist', 'ui-test', 'chrome-extension') 7 | 8 | const manifest = require('../chrome-extension/manifest.json') 9 | 10 | const addContentPermission = (manifest) => { 11 | return { 12 | ...manifest, 13 | "content_scripts": [ 14 | { 15 | "matches": [ 16 | "" 17 | ], 18 | "js": ["js/content.js"] 19 | } 20 | ], 21 | } 22 | } 23 | 24 | fs.removeSync(DIST_DIR) 25 | 26 | fs.ensureDirSync(DIST_DIR) 27 | 28 | fs.copySync(SRC_DIR, DIST_DIR) 29 | 30 | console.info('adding content_scripts in manifest.json for testing purposes') 31 | const testManifest = addContentPermission(manifest) 32 | 33 | const testManifestPath = path.join(DIST_DIR, 'manifest.json') 34 | 35 | fs.writeFileSync(testManifestPath, JSON.stringify(testManifest, null, 2)) 36 | -------------------------------------------------------------------------------- /chrome-extension/js/gtm-tags.js: -------------------------------------------------------------------------------- 1 | (function (dataLayer) { 2 | var xhrOpen = window.XMLHttpRequest.prototype.open 3 | var xhrSend = window.XMLHttpRequest.prototype.send 4 | window.XMLHttpRequest.prototype.open = function () { 5 | this.method = arguments[0] 6 | this.url = arguments[1] 7 | return xhrOpen.apply(this, [].slice.call(arguments)) 8 | } 9 | window.XMLHttpRequest.prototype.send = function () { 10 | var xhr = this 11 | var intervalId = window.setInterval(function () { 12 | if (xhr.readyState !== 4) { 13 | return 14 | } 15 | try { 16 | var data = { 17 | event: 'ajaxSuccess', 18 | eventCategory: 'AJAX', 19 | eventAction: 'jira', 20 | eventLabel: xhr.method 21 | } 22 | dataLayer.push(data) 23 | } catch (e) { 24 | console.debug('GTM error', e) 25 | } finally { 26 | clearInterval(intervalId) 27 | } 28 | }, 10) 29 | return xhrSend.apply(this, [].slice.call(arguments)) 30 | } 31 | })(window.dataLayer) 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alfeu Santos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/integration/browser-manager.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer') 2 | const CRX_PATH = `${process.cwd()}/dist/ui-test/chrome-extension/` 3 | 4 | module.exports = (() => { 5 | return { 6 | getBrowser: async () => { 7 | return puppeteer.launch({ 8 | headless: false, // extensions only supported in full chrome. 9 | args: [ 10 | `--disable-extensions-except=${CRX_PATH}`, 11 | `--load-extension=${CRX_PATH}`, 12 | '--user-agent=PuppeteerAgent' 13 | ] 14 | }) 15 | }, 16 | getExtensionInfo: async (browser) => { 17 | const page = await browser.newPage() 18 | 19 | // go to any page, so that the content.js can inject the extension id in a new element 20 | await page.goto('https://www.google.com/') 21 | const extensionId = await page.evaluate(() => document.getElementById('jiraworklog_id').value) 22 | const popupUrl = `chrome-extension://${extensionId}/popup.html` 23 | const optionsUrl = `chrome-extension://${extensionId}/options.html` 24 | 25 | console.log({ popupUrl }) 26 | return { 27 | extensionId, 28 | popupUrl, 29 | optionsUrl 30 | } 31 | } 32 | } 33 | })() 34 | -------------------------------------------------------------------------------- /chrome-extension/js/update-script.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | var updateScript = (function () { 3 | function saveOptions (jiraOptions) { 4 | return new Promise(resolve => { 5 | chrome.storage.sync.set( 6 | { 7 | jiraOptions: jiraOptions 8 | }, 9 | function () { 10 | resolve() 11 | } 12 | ) 13 | }) 14 | } 15 | function getOptions () { 16 | return new Promise(resolve => { 17 | chrome.storage.sync.get( 18 | { 19 | jiraOptions: {} 20 | }, 21 | function (options) { 22 | resolve(options.jiraOptions) 23 | } 24 | ) 25 | }) 26 | } 27 | function removePassword () { 28 | var getPromise = getOptions() 29 | var savePromise = getPromise.then((jiraOptions) => { 30 | jiraOptions.password = '' 31 | return saveOptions(jiraOptions) 32 | }) 33 | return savePromise 34 | } 35 | return { 36 | run: () => { 37 | // Check whether new version is installed 38 | var thisVersion = chrome.runtime.getManifest().version 39 | console.log(`app version: ${thisVersion}`) 40 | return removePassword().then(() => { 41 | return Promise.resolve() 42 | }) 43 | } 44 | } 45 | })() 46 | 47 | if (typeof module !== 'undefined') { module.exports = updateScript } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/settings.json 3 | !.vscode/tasks.json 4 | !.vscode/launch.json 5 | !.vscode/extensions.json 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | oauth.txt 66 | 67 | # package output 68 | chrome-extension.zip 69 | 70 | #extension key 71 | chrome-extension.crx 72 | chrome-extension.pem 73 | 74 | # jest junit 75 | junit.xml 76 | 77 | # dist/build 78 | 79 | dist/ 80 | -------------------------------------------------------------------------------- /tests/unit/update-script.unit.spec.js: -------------------------------------------------------------------------------- 1 | /* global test jest expect describe beforeEach */ 2 | const updateScript = require('../../chrome-extension/js/update-script') 3 | 4 | describe('Update Script', () => { 5 | beforeEach(() => { 6 | // arrange 7 | global.options = { 8 | jiraOptions: { 9 | jiraUrl: 'https://whatever.com', 10 | user: 'someuser@gmail.com', 11 | password: 'pwd', 12 | token: 'tkn' 13 | } 14 | } 15 | global.chrome = { 16 | runtime: { 17 | getManifest: jest.fn(function () { 18 | return { 19 | version: '0.2.3' 20 | } 21 | }) 22 | }, 23 | storage: { 24 | sync: { 25 | set: jest.fn((options, callback) => { 26 | global.options = options 27 | callback() 28 | }), 29 | get: jest.fn((jiraOptions, callback) => { 30 | callback(global.options) 31 | }) 32 | } 33 | } 34 | } 35 | global.console = { 36 | log: jest.fn() 37 | } 38 | }) 39 | 40 | test('should run update storage script upon extension update', (done) => { 41 | // act 42 | // console.log('before', options); 43 | expect(global.options.jiraOptions.password).toBe('pwd') 44 | updateScript.run().then(() => { 45 | // assert 46 | 47 | expect(console.log).toHaveBeenCalledWith('app version: 0.2.3') 48 | expect(global.chrome.runtime.getManifest).toHaveBeenCalled() 49 | expect(global.options.jiraOptions.token).toBe('tkn') 50 | expect(global.options.jiraOptions.password).toBe('') 51 | 52 | done() 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/node:14.19.3-browsers 6 | steps: 7 | - checkout 8 | - run: 9 | name: Install Headless Chrome dependencies 10 | command: | 11 | sudo apt-get install -yq \ 12 | gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ 13 | libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ 14 | libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \ 15 | libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \ 16 | fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget || sudo apt-get -f install 17 | - restore_cache: 18 | name: Restore Node Package Cache 19 | keys: 20 | # when lock file changes, use increasingly general patterns to restore cache 21 | - node-v3-{{ .Branch }}-{{ checksum "package-lock.json" }} 22 | - node-v3-{{ .Branch }}- 23 | - node-v3- 24 | - run: 25 | name: Install Dependencies 26 | command: npm i 27 | - save_cache: 28 | name: Save Node Package Cache 29 | paths: 30 | - ~/project/node_modules 31 | key: node-v2-{{ .Branch }}-{{ checksum "package-lock.json" }} 32 | - run: npm run lint 33 | - run: 34 | name: Run tests with JUnit as reporter 35 | command: xvfb-run -a npm run test -- --ci --reporters=default --reporters=jest-junit 36 | environment: 37 | JEST_JUNIT_OUTPUT_DIR: ./reports/junit/ 38 | - store_test_results: 39 | path: ./reports/junit/ 40 | - store_artifacts: 41 | path: ./reports/junit 42 | -------------------------------------------------------------------------------- /chrome-extension/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jira Worklog Tool Options 6 | 7 | 32 | 33 | 34 |
35 |

Configure Jira Connection

36 |

Make sure you are logged in jira in this browser, then configure your Jira Hostname and press Test Connection.

37 | 38 | 39 |
40 | 41 |
42 |
43 |

Basic Authentication*

44 |

*You probably don't need it. Only fill it if the Jira API URL is different from the Jira Website you use, or if connection doesn't work even after logging in on Jira through Chrome.

45 | 46 | 47 |
48 | 49 | 50 |
51 |

App Token*

52 |

*Fill only if required by your Jira API. Ask your IT department for assistance.

53 | 54 | 55 | 56 |
57 | 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira-log", 3 | "version": "0.5.1", 4 | "description": "", 5 | "main": "jira-parser.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "unit": "jest -c jest.config.unit.json", 11 | "integration": "jest -c jest.config.integration.json", 12 | "test": "npm run build-test && jest -c jest.config.json", 13 | "test:watch": "jest -c jest.config.unit.json --watch", 14 | "test:coverage": "jest -c jest.config.unit.json --coverage", 15 | "lint": "npm run lint:code && npm run lint:tests", 16 | "lint:code": "eslint --config .eslintrc.json --ignore-path .eslintignore chrome-extension/**/*.js --fix --quiet", 17 | "lint:tests": "eslint --config .eslintrc.json --ignore-path .eslintignore tests/**/*.spec.js --fix --quiet", 18 | "package": "node scripts/package", 19 | "deploy": "npm run test && npm run sync-version && npm run package && node scripts/deploy", 20 | "major": "npm version --no-git-tag-version major && npm run sync-version", 21 | "minor": "npm version --no-git-tag-version minor && npm run sync-version", 22 | "patch": "npm version --no-git-tag-version patch && npm run sync-version", 23 | "sync-version": "node scripts/sync-version", 24 | "build-test": "node scripts/build-for-test", 25 | "ui-test-playground": "npm run build-test && node tests/integration/test-puppeteer" 26 | }, 27 | "author": "Alfeu Santos ", 28 | "license": "MIT", 29 | "jest": { 30 | "testEnvironment": "jsdom" 31 | }, 32 | "devDependencies": { 33 | "archiver": "^2.1.1", 34 | "chrome-webstore-upload": "^0.2.2", 35 | "dotenv": "^4.0.0", 36 | "eslint": "^6.8.0", 37 | "eslint-config-standard": "^13.0.1", 38 | "eslint-plugin-import": "^2.18.2", 39 | "eslint-plugin-node": "^9.1.0", 40 | "eslint-plugin-promise": "^4.2.1", 41 | "eslint-plugin-standard": "^4.0.0", 42 | "fs-extra": "^10.0.0", 43 | "hoek": "4.2.1", 44 | "jest": "^26.6.3", 45 | "jest-cli": "^26.6.3", 46 | "jest-junit": "^12.2.0", 47 | "puppeteer": "^1.19.0", 48 | "sshpk": "1.14.1" 49 | }, 50 | "repository": "https://github.com/alfeugds/jiraworklogtool.git", 51 | "dependencies": { 52 | "axios": "^0.21.4", 53 | "axios-mock-adapter": "^1.15.0", 54 | "retire": "^1.6.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/integration/jira-mock.js: -------------------------------------------------------------------------------- 1 | const defaultSuccessfulResponse = { 2 | status: 200, 3 | contentType: 'application/json;charset=UTF-8', 4 | headers: { 5 | // 'x-ausername': 'hue@br.com', 6 | 'x-aaccountid': 'some:token' 7 | } 8 | } 9 | const defaultFailedResponse = { 10 | status: 404 11 | } 12 | const search = { 13 | successfulGETRequestWithTwoItems: Object.assign({ 14 | body: JSON.stringify({ 15 | issues: [{ 16 | 'key': 'CMS-123' 17 | },{ 18 | 'key': 'CMS-456' 19 | }] 20 | }) 21 | }, defaultSuccessfulResponse) 22 | //TODO: mock //https://jira.com/rest/api/2/search?fields=fields,key&jql=worklogDate=%272018-01-01%27%20AND%20worklogAuthor=currentUser() 23 | }; 24 | const items = { 25 | "CMS-123": { 26 | "worklogs":[ 27 | { 28 | "author":{ 29 | // "key":"hue@br.com", 30 | "accountId": "some:token" 31 | }, 32 | "comment":"tech onboarding", 33 | "started":"2018-01-01T06:00:00.000+0000", 34 | "timeSpent":"1h 50m", 35 | "id":"55829" 36 | } 37 | ] 38 | }, 39 | "CMS-456": { 40 | "worklogs":[ 41 | { 42 | "author":{ 43 | // "key":"hue@br.com", 44 | "accountId": "some:token" 45 | }, 46 | "comment":"tech onboarding 2", 47 | "started":"2018-01-01T06:00:00.000+0000", 48 | "timeSpent":"2h 50m", 49 | "id":"45645" 50 | } 51 | ] 52 | } 53 | } 54 | 55 | module.exports = { 56 | getResponse: (request) => { 57 | if(!request.url().includes('https://jira.com/')) 58 | return defaultFailedResponse; 59 | //search 60 | if (request.url().includes('rest/api/2/search?fields=fields,key&jql=')) 61 | return search.successfulGETRequestWithTwoItems; 62 | 63 | let match; 64 | if(match = request.url().match('rest/api/2/issue/([^/]+)/worklog')){ 65 | let item = match[1] 66 | return Object.assign( {body: JSON.stringify(items[item])}, defaultSuccessfulResponse); 67 | } 68 | 69 | return { 70 | status: 404 71 | }; 72 | } 73 | }; -------------------------------------------------------------------------------- /chrome-extension/js/jira-parser.js: -------------------------------------------------------------------------------- 1 | var JiraParser = (function () { 2 | const timeRegex = new RegExp('(\\d+[m]|\\d+[d](?:(?:\\s\\d+[h])?(?:\\s\\d+[m])?)?|\\d+[h](?:\\s\\d+[m])?)') 3 | const hoursAndMinutesRegex = new RegExp('^' + timeRegex.source + '$') 4 | const jiraRegex = new RegExp('([a-zA-Z0-9][a-zA-Z0-9_-]*?-\\d+)') 5 | const jiraNumberRegex = new RegExp('^' + jiraRegex.source + '$') 6 | const worklogTextLineRegex = new RegExp('\\b' + jiraRegex.source + '?\\b.*?\\b' + timeRegex.source + '\\b[\\s\\-_;,]*(.+)$') 7 | 8 | function timeSpentToHours (timeSpent) { 9 | let result = 0 10 | let match 11 | if (timeSpent.indexOf('d') > -1) { 12 | match = /\b(\d+)d\b/.exec(timeSpent) 13 | if (match) { 14 | var d = match[1] 15 | result += parseFloat(d.replace('d', '')) * 8 16 | } 17 | } 18 | if (timeSpent.indexOf('h') > -1) { 19 | match = /\b(\d+)h\b/.exec(timeSpent) 20 | if (match) { 21 | var h = match[1] 22 | result += parseFloat(h.replace('h', '')) 23 | } 24 | } 25 | if (timeSpent.indexOf('m') > -1) { 26 | match = /\b(\d+)m\b/.exec(timeSpent) 27 | if (match) { 28 | var m = match[1] 29 | result += parseFloat(m.replace('m', '')) / 60 30 | } 31 | } 32 | return result 33 | } 34 | 35 | function isValidTimeSpentFormat (timeSpent) { 36 | if (hoursAndMinutesRegex.exec(timeSpent)) { return true } else { return false } 37 | } 38 | 39 | function parse (text) { 40 | let hoursAndMinutes = '' 41 | let jiraNumber = '' 42 | let worklog = text 43 | 44 | const matches = worklogTextLineRegex.exec(text) 45 | 46 | if (matches) { 47 | jiraNumber = matches[1] || '' 48 | hoursAndMinutes = matches[2] || '' 49 | worklog = matches[3] || worklog 50 | } 51 | 52 | const result = { 53 | timeSpent: hoursAndMinutes, 54 | jira: jiraNumber, 55 | comment: worklog 56 | } 57 | return result 58 | } 59 | 60 | function getInvalidFields (item) { 61 | var result = [] 62 | if (!jiraNumberRegex.exec(item.jira)) { 63 | result.push('jira') 64 | } 65 | if (!isValidTimeSpentFormat(item.timeSpent)) { 66 | result.push('timeSpent') 67 | } 68 | if (!item.comment.trim()) { 69 | result.push('comment') 70 | } 71 | return result 72 | } 73 | 74 | return { 75 | parse: parse, 76 | timeSpentToHours: timeSpentToHours, 77 | isValidTimeSpentFormat: isValidTimeSpentFormat, 78 | getInvalidFields: getInvalidFields 79 | } 80 | })() 81 | 82 | if (typeof module !== 'undefined') { module.exports = JiraParser } 83 | -------------------------------------------------------------------------------- /chrome-extension/js/options.js: -------------------------------------------------------------------------------- 1 | var chrome = window.chrome || {} 2 | var JiraHelper = window.JiraHelper || {} 3 | 4 | // Saves options to chrome.storage 5 | function saveOptions (options) { 6 | // make sure to not save user password, as chrome storage is not encrypted (https://developer.chrome.com/apps/storage#using-sync). 7 | // The JESSIONID authentication cookie will be remembered by the browser once User clicks 'Test Connection' anyway, 8 | // and Jira will consider the JESSIONID cookie and ignore the basic auth settings for the requests. 9 | options.password = '' 10 | 11 | chrome.storage.sync.set( 12 | { 13 | jiraOptions: options 14 | }, 15 | function () { 16 | // Update status to let user know options were saved. 17 | var status = document.getElementById('status') 18 | status.textContent = 'Options saved.' 19 | setTimeout(function () { 20 | status.textContent = '' 21 | }, 1500) 22 | } 23 | ) 24 | } 25 | 26 | // Restores options state using the preferences 27 | // stored in chrome.storage. 28 | function restoreOptions () { 29 | chrome.storage.sync.get( 30 | { 31 | jiraOptions: {} 32 | }, 33 | function (items) { 34 | restoreOptionsToInput(items.jiraOptions) 35 | } 36 | ) 37 | } 38 | 39 | var jiraUrlInput, userInput, passwordInput, tokenInput 40 | 41 | function restoreOptionsToInput (options) { 42 | jiraUrlInput.value = options.jiraUrl || '' 43 | userInput.value = options.user || '' 44 | passwordInput.value = options.password || '' 45 | tokenInput.value = options.token || '' 46 | } 47 | 48 | function getOptionsFromInput () { 49 | return { 50 | jiraUrl: jiraUrlInput.value, 51 | user: userInput.value, 52 | password: passwordInput.value, 53 | token: tokenInput.value 54 | } 55 | } 56 | 57 | document.addEventListener('DOMContentLoaded', () => { 58 | restoreOptions() 59 | jiraUrlInput = document.getElementById('jiraUrl') 60 | userInput = document.getElementById('user') 61 | passwordInput = document.getElementById('password') 62 | tokenInput = document.getElementById('token') 63 | 64 | document.getElementById('save').addEventListener('click', () => { 65 | saveOptions(getOptionsFromInput()) 66 | }) 67 | document.getElementById('testConnection').addEventListener('click', () => { 68 | var jiraOptions = getOptionsFromInput() 69 | console.log(jiraOptions) 70 | JiraHelper.testConnection(jiraOptions) 71 | .then(result => { 72 | console.info('connection successful', result) 73 | saveOptions(getOptionsFromInput()) 74 | alert('Connection [OK]') 75 | }) 76 | .catch(error => { 77 | console.error('connection failed', error) 78 | alert('Connection [FAILED]. Please double-check the options.') 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | /* In order for the deploy script to work, a .env file must be created. 7 | 1- acces OAuth client console and get cliend id and secret 8 | 2- access https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=113688225649-ttrnqlh7f18d9cngcsu4e4erp0226jpp.apps.googleusercontent.com&redirect_uri=urn:ietf:wg:oauth:2.0:oob 9 | 3- get token 10 | 4- run command below in a browser console(ref: https://github.com/DrewML/chrome-webstore-upload/blob/master/How%20to%20generate%20Google%20API%20keys.md): 11 | copy(`curl "https://accounts.google.com/o/oauth2/token" -d "client_id=${encodeURIComponent(prompt('Enter your clientId'))}&client_secret=${encodeURIComponent(prompt('Enter your clientSecret'))}&code=${encodeURIComponent(prompt('Enter your authcode'))}&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob"`);alert('The curl has been copied. Paste it into your terminal.') 12 | 5- run copied curl 13 | 6- update .env file 14 | */ 15 | 16 | const webStore = require('chrome-webstore-upload')({ 17 | extensionId: 'pekbjnkonfmgjfnbpmindidammhgmjji', 18 | clientId: process.env.CHROME_CLIENT_ID, 19 | clientSecret: process.env.CHROME_CLIENT_SECRET, 20 | refreshToken: process.env.CHROME_REFRESH_TOKEN 21 | }); 22 | 23 | console.log('Deploying to Chrome Web Store...'); 24 | console.log(`CHROME_CLIENT_ID: ${process.env.CHROME_CLIENT_ID}`); 25 | console.log(`CHROME_CLIENT_SECRET: ${process.env.CHROME_CLIENT_SECRET}`); 26 | console.log(`CHROME_REFRESH_TOKEN: ${process.env.CHROME_REFRESH_TOKEN}`); 27 | 28 | function getToken(){ 29 | const token = process.env.CHROME_ACCESS_TOKEN; 30 | if(token) 31 | return Promise.resolve(token); 32 | else { 33 | console.log('fetching token...'); 34 | return webStore.fetchToken(); 35 | } 36 | } 37 | (async () => { 38 | try{ 39 | const token = await getToken(); 40 | //upload 41 | const zipPath = path.join(__dirname, '../chrome-extension.zip'); 42 | const myZipFile = fs.createReadStream(zipPath); 43 | try{ 44 | console.log('uploading...'); 45 | let res = await webStore.uploadExisting(myZipFile, token); 46 | 47 | console.log('publishing...'); 48 | const target = 'default'; 49 | res = await webStore.publish(target, token); 50 | console.log('Chrome publish complete!', res); 51 | 52 | } catch (err) { 53 | console.error('Chrome upload failed: ', err); 54 | process.exitCode = 1; 55 | } 56 | }catch(err) { 57 | console.error('fetching token failed: ', err); 58 | process.exitCode = 1; 59 | }; 60 | })(); -------------------------------------------------------------------------------- /tests/integration/test-puppeteer.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer') 2 | const browserManager = require('./browser-manager') 3 | 4 | const CRX_PATH = `${process.cwd()}/chrome-extension/` 5 | const jiraMock = require('./jira-mock') 6 | const CHROME_EXTENSION_URL = 'chrome-extension://ehkgicpgemphledafbkdenjjekkogbmk/' 7 | 8 | console.log(jiraMock); 9 | 10 | (async () => { 11 | const POPUP_PAGE = `${CHROME_EXTENSION_URL}popup.html` 12 | const browser = await browserManager.getBrowser() 13 | const { popupUrl } = await browserManager.getExtensionInfo(browser) 14 | // ... do some testing ... 15 | 16 | // const extensionPage = await browser.newPage(); 17 | // await extensionPage.goto('chrome://extensions/') 18 | 19 | const page = await browser.newPage() 20 | page.on('dialog', async dialog => { 21 | console.log(dialog.message()) 22 | await dialog.accept() 23 | }) 24 | 25 | await page.setRequestInterception(true) 26 | page.on('request', request => { 27 | if (request.url().includes('chrome-extension://')) { 28 | request.continue() 29 | } else { 30 | request.respond(jiraMock.getResponse(request)) 31 | } 32 | }) 33 | await page.goto(popupUrl) 34 | 35 | // await page.goto(POPUP_PAGE) 36 | // click buttons, test UI elements, etc. 37 | const errorMessage = await page.evaluate(() => document.querySelector('.error_status h2').textContent) 38 | console.log(errorMessage) 39 | await page.waitFor(200) 40 | await page.click('h2>a') 41 | await page.waitFor(1000) 42 | 43 | const pages = await browser.pages() 44 | optionsPage = pages.filter(p => p.url().includes('options.html'))[0] 45 | optionsPage.on('dialog', async dialog => { 46 | console.log(dialog.message()) 47 | await dialog.accept() 48 | }) 49 | 50 | await optionsPage.setRequestInterception(true) 51 | optionsPage.on('request', request => { 52 | console.log(request) 53 | request.respond(jiraMock.getResponse(request)) 54 | }) 55 | 56 | await optionsPage.waitFor(500) 57 | await optionsPage.type('#jiraUrl', 'https://jira.com') 58 | await optionsPage.click('#testConnection') 59 | await optionsPage.click('#save') 60 | // await optionsPage.reload(); 61 | // await page.click('h2>a'); 62 | await page.bringToFront() 63 | await page.waitFor(100) 64 | await page.reload() 65 | await page.type('#worklogDate', '01/01/2018') 66 | await page.waitFor(100) 67 | async function getValueArrayFromInputs (page, selector) { 68 | return page.evaluate((selector) => 69 | Array.from(document.querySelectorAll(selector)) 70 | .map((i) => i.value), selector) 71 | } 72 | const jiraNumberArray = await getValueArrayFromInputs(page, 'input[name=jira]') 73 | console.log(jiraNumberArray) 74 | const timeSpentArray = await getValueArrayFromInputs(page, 'input[name=timeSpent]') 75 | console.log(timeSpentArray) 76 | const commentArray = await getValueArrayFromInputs(page, 'input[name=comment]') 77 | console.log(commentArray) 78 | 79 | // await browser.close(); 80 | })() 81 | -------------------------------------------------------------------------------- /chrome-extension/js/model.js: -------------------------------------------------------------------------------- 1 | /* global mediator chrome */ 2 | window.Model = {} 3 | window.Model.WorklogModel = (function (JiraParser) { 4 | var items = [] 5 | var totalHours = 0.0 6 | 7 | function addAll (newItems) { 8 | newItems.forEach(function (item) { 9 | item.status = 'new' 10 | }, this) 11 | 12 | items = items.concat(newItems) 13 | mediator.trigger('model.workloglist.updated', items) 14 | updateTotalHours() 15 | } 16 | 17 | function getItems () { 18 | return items 19 | } 20 | 21 | function updateItemsFromJira (newItems) { 22 | items = items.filter(item => { 23 | return item.status !== 'saved' && item.status !== 'edited' && item.status !== 'deleted' 24 | }) 25 | 26 | items = items.concat(newItems) 27 | mediator.trigger('model.workloglist.updated', items) 28 | updateTotalHours() 29 | } 30 | 31 | function updateItemsWithLocalData (persistedItems) { 32 | items = items.filter(item => { 33 | return item.status !== 'new' 34 | }) 35 | 36 | items = items.concat(persistedItems) 37 | updateTotalHours() 38 | } 39 | 40 | function updateTotalHours () { 41 | var total = 0.0 42 | for (var i = 0; i < items.length; i++) { 43 | var worklog = items[i] 44 | total += JiraParser.timeSpentToHours(worklog.timeSpent) 45 | } 46 | totalHours = total 47 | mediator.trigger('modal.totalHours.update', totalHours) 48 | } 49 | 50 | function clearItems () { 51 | items = [] 52 | } 53 | 54 | function getTotalHours () { 55 | updateTotalHours() 56 | return totalHours 57 | } 58 | 59 | function persistUnsavedWorklogToLocal (date, worklogs) { 60 | return new Promise((resolve) => { 61 | getUnsavedWorklogFromLocal().then(persistedWorklogs => { 62 | // save only new items 63 | worklogs = worklogs.filter(item => { 64 | return item.status === 'new' 65 | }) 66 | 67 | worklogs.forEach(function (item) { 68 | item.started = date 69 | }, this) 70 | 71 | persistedWorklogs = persistedWorklogs.filter(function (item) { 72 | return item.started !== date 73 | }, this) 74 | 75 | persistedWorklogs = persistedWorklogs.concat(worklogs) 76 | // ... 77 | chrome.storage.local.set({ 78 | worklogs: persistedWorklogs 79 | }, () => { 80 | resolve() 81 | }) 82 | }) 83 | }) 84 | } 85 | 86 | function getUnsavedWorklogFromLocal (date) { 87 | return new Promise((resolve) => { 88 | chrome.storage.local.get({ 89 | worklogs: [] 90 | }, result => { 91 | var worklogs = result.worklogs 92 | if (date) { 93 | worklogs = worklogs.filter(item => { 94 | return item.started === date 95 | }) 96 | } 97 | console.log('getUnsavedWorklogFromLocal() result:', worklogs) 98 | resolve(worklogs) 99 | }) 100 | }) 101 | } 102 | 103 | return { 104 | addAll: addAll, 105 | getItems: getItems, 106 | getTotalHours: getTotalHours, 107 | updateItemsFromJira: updateItemsFromJira, 108 | getUnsavedWorklogFromLocal: getUnsavedWorklogFromLocal, 109 | persistUnsavedWorklogToLocal: persistUnsavedWorklogToLocal, 110 | updateItemsWithLocalData: updateItemsWithLocalData, 111 | clearItems: clearItems 112 | } 113 | })(window.JiraParser) 114 | -------------------------------------------------------------------------------- /tests/integration/uitest.int.spec.js: -------------------------------------------------------------------------------- 1 | const browserManager = require('./browser-manager') 2 | const { 3 | getOptionsPage, 4 | getPopupPage, 5 | makeSureJiraUrlIsConfigured 6 | } = require('./jira-extension-functions') 7 | 8 | describe('UI Test', () => { 9 | describe('popup', () => { 10 | let browser 11 | let extensionInfo 12 | beforeEach(async () => { 13 | // browser must be initialized 14 | browser = await browserManager.getBrowser() 15 | extensionInfo = extensionInfo || await browserManager.getExtensionInfo(browser) 16 | }) 17 | afterEach(async () => { 18 | await browser.close() 19 | }) 20 | test('Loads successfully with no worklogs', async (done) => { 21 | try { 22 | const popup = await getPopupPage(browser, extensionInfo) 23 | const errorMessage = await popup.getErrorMessage() 24 | expect(errorMessage).toEqual('Please go to Options and make sure you are logged in Jira, and the Jira Hostname is correct.') 25 | await popup.clickOptionsPage() 26 | await popup.wait() 27 | 28 | const optionsPage = await getOptionsPage(browser) 29 | await optionsPage.setValidJiraUrl() 30 | await optionsPage.clickOnTestconnection() 31 | const connectionResultMessage = await optionsPage.waitForTestConnectionResult() 32 | expect(connectionResultMessage).toEqual('Connection [OK]') 33 | done() 34 | } catch (e) { 35 | fail(e) 36 | done() 37 | } 38 | }) 39 | test('Loads successfully with some worklogs', async done => { 40 | try { 41 | await makeSureJiraUrlIsConfigured(browser, extensionInfo) 42 | const popupPage = await getPopupPage(browser, extensionInfo) 43 | await popupPage.wait() 44 | await popupPage.setWorklogDate('01/01/2018') 45 | const savedJiraArray = await popupPage.getJiraArray() 46 | const commentArray = await popupPage.getCommentArray() 47 | const timeSpentArray = await popupPage.getTimeSpentArray() 48 | expect(savedJiraArray).toEqual(['CMS-123', 'CMS-456']) 49 | expect(timeSpentArray).toEqual(['1h 50m', '2h 50m']) 50 | expect(commentArray).toEqual(['tech onboarding', 'tech onboarding 2']) 51 | 52 | done() 53 | } catch (e) { 54 | fail(e) 55 | done() 56 | } 57 | }) 58 | test('Adds some worklogs from text', async done => { 59 | // given I have the extension opened and with jira configured 60 | await makeSureJiraUrlIsConfigured(browser, extensionInfo) 61 | const popup = await getPopupPage(browser, extensionInfo) 62 | await popup.wait() 63 | // and I select the worklog date as 01/01/2018 64 | await popup.setWorklogDate('01/01/2018') 65 | // then I see the total worklog as 4.67h 66 | let totalWorklog = await popup.getTotalWorklog() 67 | expect(totalWorklog).toEqual('4.67h') 68 | // when I write down the following worklog 69 | const worklogText = '30m some worklog' 70 | await popup.setWorklogText(worklogText) 71 | // then I should see the worklogs in the worklog table 72 | const commentArray = await popup.getCommentArray() 73 | expect(commentArray.sort()).toEqual(['tech onboarding', 'tech onboarding 2', 'some worklog'].sort()) 74 | // and I should see the total worklog time updated to 5.17h 75 | totalWorklog = await popup.getTotalWorklog() 76 | expect(totalWorklog).toEqual('5.17h') 77 | 78 | done() 79 | }) 80 | test.todo('POSTs some worklogs') 81 | test.todo('PUTs some worklogs') 82 | test.todo('DELETEs some worklogs') 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /chrome-extension/lib/mediator.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"use strict";"function"==typeof define&&define.amd?define("mediator-js",[],function(){return a.Mediator=b(),a.Mediator}):"undefined"!=typeof exports?exports.Mediator=b():a.Mediator=b()}(this,function(){"use strict";function a(){var a=function(){return(0|65536*(1+Math.random())).toString(16).substring(1)};return a()+a()+"-"+a()+"-"+a()+"-"+a()+"-"+a()+a()+a()}function b(c,d,e){return this instanceof b?(this.id=a(),this.fn=c,this.options=d,this.context=e,this.channel=null,void 0):new b(c,d,e)}function c(a,b){return this instanceof c?(this.namespace=a||"",this._subscribers=[],this._channels={},this._parent=b,this.stopped=!1,void 0):new c(a)}function d(){return this instanceof d?(this._channels=new c(""),void 0):new d}return b.prototype={update:function(a){a&&(this.fn=a.fn||this.fn,this.context=a.context||this.context,this.options=a.options||this.options,this.channel&&this.options&&void 0!==this.options.priority&&this.channel.setPriority(this.id,this.options.priority))}},c.prototype={addSubscriber:function(a,c,d){var e=new b(a,c,d);return c&&void 0!==c.priority?(c.priority=c.priority>>0,c.priority<0&&(c.priority=0),c.priority>=this._subscribers.length&&(c.priority=this._subscribers.length-1),this._subscribers.splice(c.priority,0,e)):this._subscribers.push(e),e.channel=this,e},stopPropagation:function(){this.stopped=!0},getSubscriber:function(a){var b=0,c=this._subscribers.length;for(c;c>b;b++)if(this._subscribers[b].id===a||this._subscribers[b].fn===a)return this._subscribers[b]},setPriority:function(a,b){var e,f,g,h,c=0,d=0;for(d=0,h=this._subscribers.length;h>d&&this._subscribers[d].id!==a&&this._subscribers[d].fn!==a;d++)c++;e=this._subscribers[c],f=this._subscribers.slice(0,c),g=this._subscribers.slice(c+1),this._subscribers=f.concat(g),this._subscribers.splice(b,0,e)},addChannel:function(a){this._channels[a]=new c((this.namespace?this.namespace+":":"")+a,this)},hasChannel:function(a){return this._channels.hasOwnProperty(a)},returnChannel:function(a){return this._channels[a]},removeSubscriber:function(a){var b=this._subscribers.length-1;if(!a)return this._subscribers=[],void 0;for(b;b>=0;b--)(this._subscribers[b].fn===a||this._subscribers[b].id===a)&&(this._subscribers[b].channel=null,this._subscribers.splice(b,1))},publish:function(a){var e,g,h,b=0,c=this._subscribers.length,d=!1;for(c;c>b;b++)d=!1,e=this._subscribers[b],this.stopped||(g=this._subscribers.length,void 0!==e.options&&"function"==typeof e.options.predicate?e.options.predicate.apply(e.context,a)&&(d=!0):d=!0),d&&(e.options&&void 0!==e.options.calls&&(e.options.calls--,e.options.calls<1&&this.removeSubscriber(e.id)),e.fn.apply(e.context,a),h=this._subscribers.length,c=h,h===g-1&&b--);this._parent&&this._parent.publish(a),this.stopped=!1}},d.prototype={getChannel:function(a,b){var c=this._channels,d=a.split(":"),e=0,f=d.length;if(""===a)return c;if(d.length>0)for(f;f>e;e++){if(!c.hasChannel(d[e])){if(b)break;c.addChannel(d[e])}c=c.returnChannel(d[e])}return c},subscribe:function(a,b,c,d){var e=this.getChannel(a||"",!1);return c=c||{},d=d||{},e.addSubscriber(b,c,d)},once:function(a,b,c,d){return c=c||{},c.calls=1,this.subscribe(a,b,c,d)},getSubscriber:function(a,b){var c=this.getChannel(b||"",!0);return c.namespace!==b?null:c.getSubscriber(a)},remove:function(a,b){var c=this.getChannel(a||"",!0);return c.namespace!==a?!1:(c.removeSubscriber(b),void 0)},publish:function(a){var b=this.getChannel(a||"",!0);if(b.namespace!==a)return null;var c=Array.prototype.slice.call(arguments,1);c.push(b),b.publish(c)}},d.prototype.on=d.prototype.subscribe,d.prototype.bind=d.prototype.subscribe,d.prototype.emit=d.prototype.publish,d.prototype.trigger=d.prototype.publish,d.prototype.off=d.prototype.remove,d.Channel=c,d.Subscriber=b,d.version="0.9.8",d}); 2 | -------------------------------------------------------------------------------- /chrome-extension/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 10px; 3 | } 4 | 5 | h1 { 6 | font-size: 15px; 7 | } 8 | 9 | .container { 10 | align-items: center; 11 | justify-content: space-between; 12 | padding: 10px; 13 | max-height: 548px; 14 | max-width: 768px; 15 | margin: 0 auto; 16 | } 17 | 18 | .container>* { 19 | margin: 10px 0; 20 | } 21 | 22 | .container>*:first-child { 23 | margin-top: 0px; 24 | } 25 | 26 | .container.container--hidden { 27 | display: none; 28 | } 29 | 30 | .worklog-items-table { 31 | border-collapse: collapse; 32 | border-spacing: 0; 33 | width: 100%; 34 | } 35 | 36 | .worklog-items-table td { 37 | font-family: Arial, sans-serif; 38 | font-size: 14px; 39 | padding: 5px 5px; 40 | border-style: solid; 41 | border-width: 1px; 42 | overflow: hidden; 43 | word-break: normal; 44 | } 45 | 46 | .worklog-items-table th { 47 | font-family: Arial, sans-serif; 48 | font-size: 14px; 49 | font-weight: bold; 50 | padding: 10px 5px; 51 | border-style: solid; 52 | border-width: 1px; 53 | overflow: hidden; 54 | word-break: normal; 55 | } 56 | 57 | .worklog-items-table .table-line { 58 | vertical-align: top 59 | } 60 | 61 | .worklog-items-table .jira-number-column-item { 62 | max-width: 70px; 63 | } 64 | 65 | .worklog-items-table .time-spent-column-item { 66 | max-width: 90px; 67 | } 68 | 69 | .worklog-items-table .comment-column-item { 70 | min-width: 300px; 71 | max-width: 500px; 72 | } 73 | 74 | .worklog-items-table input { 75 | width: 100%; 76 | font-size: 12px; 77 | } 78 | .worklog-textarea 79 | { 80 | width: 100%; 81 | font-size: 12px; 82 | min-height: 70px; 83 | font-size: 10px; 84 | } 85 | 86 | .worklog.worklog--edited { 87 | background-color: #ffffd2; 88 | } 89 | 90 | .worklog.worklog--invalid { 91 | background-color: #ff9b9b; 92 | } 93 | 94 | .worklog.worklog--saved { 95 | background-color: #b1ffb8; 96 | } 97 | 98 | .worklog.worklog--deleted { 99 | background-color: #ff9b9b; 100 | } 101 | 102 | .worklog input.input--invalid{ 103 | border-color: #ff2323; 104 | } 105 | 106 | .hidden { 107 | display: none; 108 | } 109 | 110 | #loading { 111 | position: fixed; 112 | margin: auto; 113 | background-color: rgba(0, 0, 0, 0.28); 114 | top: 0px; 115 | left: 0px; 116 | width: 100%; 117 | height: 100%; 118 | } 119 | 120 | #loading img { 121 | margin: 200px auto; 122 | display: block; 123 | } 124 | 125 | 126 | .delete-button{ 127 | display: inline-block; 128 | margin: auto; 129 | width: 20px; 130 | border: 1px solid #ddd; 131 | border-bottom: 1px solid #aaa; 132 | border-right: 1px solid #aaa; 133 | background-color: #fff; 134 | background-repeat: no-repeat; 135 | background-position: center; 136 | font-size: 14px; 137 | height: 19px; 138 | line-height: 1.5em; 139 | background-image: url(/img/ic_delete_black_24dp.png); 140 | } 141 | 142 | .open-link-button{ 143 | display: inline-block; 144 | margin: auto; 145 | width: 20px; 146 | border: 1px solid #ddd; 147 | border-bottom: 1px solid #aaa; 148 | border-right: 1px solid #aaa; 149 | background-color: #fff; 150 | background-repeat: no-repeat; 151 | background-position: center; 152 | font-size: 14px; 153 | height: 19px; 154 | line-height: 1.5em; 155 | background-image: url(/img/open_in_new_window.png); 156 | background-size: cover; 157 | } 158 | 159 | .link-disabled { 160 | pointer-events: none; 161 | } 162 | 163 | .save-button { 164 | margin: 10px 0; 165 | width: 70px; 166 | height: 50px; 167 | border: 1px solid #ddd; 168 | border-bottom: 1px solid #aaa; 169 | border-right: 1px solid #aaa; 170 | background-color: #fff; 171 | background-repeat: no-repeat; 172 | background-position: center; 173 | font-size: 14px; 174 | line-height: 1.5em; 175 | background-image: url(/img/ic_cloud_upload_black_48dp.png); 176 | } 177 | 178 | .error_status { 179 | padding: 20px; 180 | } 181 | 182 | .error_status h2 { 183 | white-space: initial; 184 | width: 400px; 185 | } 186 | -------------------------------------------------------------------------------- /tests/integration/jira-extension-functions.js: -------------------------------------------------------------------------------- 1 | const jiraMock = require('./jira-mock') 2 | 3 | async function makeSureJiraUrlIsConfigured (browser, extensionInfo) { 4 | await openOptionsPage(browser, extensionInfo) 5 | const optionsPage = await getOptionsPage(browser) 6 | await optionsPage.setValidJiraUrl() 7 | await optionsPage.clickOnTestconnection() 8 | await optionsPage.waitForTestConnectionResult() 9 | await optionsPage.clickOnSave() 10 | } 11 | 12 | async function openOptionsPage (browser, extensionInfo) { 13 | const page = await browser.newPage() 14 | await page.waitFor(200) 15 | return page.goto(extensionInfo.optionsUrl) 16 | } 17 | 18 | async function getPopupPage (browser, extensionInfo) { 19 | async function getValueArrayFromInputs (page, selector) { 20 | return page.evaluate((selector) => 21 | Array.from(document.querySelectorAll(selector)) 22 | .map((i) => i.value), selector) 23 | } 24 | 25 | const page = await browser.newPage() 26 | 27 | // ignore error dialog 28 | await page.on('dialog', async dialog => { 29 | await dialog.accept() 30 | }) 31 | 32 | await page.setRequestInterception(true) 33 | await page.on('request', request => { 34 | if (request.url().includes('chrome-extension://')) { request.continue() } else { request.respond(jiraMock.getResponse(request)) } 35 | }) 36 | 37 | await page.goto(extensionInfo.popupUrl) 38 | 39 | await page.waitFor(300) 40 | 41 | return { 42 | getErrorMessage: async () => { 43 | const errorMessage = await page.evaluate(() => document.querySelector('.error_status h2').textContent) 44 | return errorMessage 45 | }, 46 | clickOptionsPage: async () => { 47 | await page.click('h2>a') 48 | return page.waitFor(100) 49 | }, 50 | wait: async () => { 51 | return page.waitFor(100) 52 | }, 53 | setWorklogDate: async (date) => { 54 | await page.type('#worklogDate', date || '01/01/2018') 55 | await page.waitFor(100) 56 | await page.click('#getWorklogButton') 57 | return page.waitFor(300) 58 | }, 59 | getJiraArray: async () => { 60 | return getValueArrayFromInputs(page, 'input[name=jira]') 61 | }, 62 | getTimeSpentArray: async () => { 63 | return getValueArrayFromInputs(page, 'input[name=timeSpent]') 64 | }, 65 | getCommentArray: async () => { 66 | return getValueArrayFromInputs(page, 'input[name=comment]') 67 | }, 68 | getWorklogText: async () => { 69 | return page.evaluate(() => document.querySelector('#worklog').value) 70 | }, 71 | setWorklogText: async worklogText => { 72 | await page.waitFor(100) 73 | await page.type('#worklog', worklogText) 74 | await page.waitFor(100) 75 | await page.click('#addWorklogs') 76 | return page.waitFor(100) 77 | }, 78 | getTotalWorklog: async () => { 79 | await page.waitFor(200) 80 | return page.evaluate(() => document.querySelector('#totalHours').textContent) 81 | } 82 | } 83 | } 84 | 85 | async function getOptionsPage (browser) { 86 | const pages = await browser.pages() 87 | let dialogDeferred = null 88 | let dialogPromise = null 89 | const page = pages.filter(p => p.url().includes('options.html'))[0] 90 | 91 | // intercept all requests 92 | await page.setRequestInterception(true) 93 | page.on('request', request => { 94 | if (request.url().includes('chrome-extension://')) { request.continue() } else { request.respond(jiraMock.getResponse(request)) } 95 | }) 96 | 97 | page.on('dialog', async dialog => { 98 | const dialogMessage = dialog.message() 99 | await dialog.accept() 100 | dialogDeferred.resolve(dialogMessage) 101 | }) 102 | 103 | return { 104 | setValidJiraUrl: async () => { 105 | await page.waitFor(100) 106 | await page.type('#jiraUrl', 'https://jira.com') 107 | return page.waitFor(300) 108 | }, 109 | clickOnTestconnection: async () => { 110 | dialogPromise = new Promise((resolve, reject) => { 111 | dialogDeferred = { 112 | resolve: resolve, 113 | reject: reject 114 | } 115 | }) 116 | await page.click('#testConnection') 117 | return page.waitFor(100) 118 | }, 119 | clickOnSave: async () => { 120 | await page.click('#save') 121 | return page.waitFor(300) 122 | }, 123 | waitForTestConnectionResult: async () => { 124 | await page.waitFor(100) 125 | return dialogPromise 126 | } 127 | } 128 | } 129 | 130 | module.exports = { 131 | makeSureJiraUrlIsConfigured, 132 | openOptionsPage, 133 | getPopupPage, 134 | getOptionsPage, 135 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jira Worklog Tool [Archived] 2 | 3 | > [!WARNING] 4 | > This repo is no longer being maintained and is currently archived. 5 | > Please see the below fork(s) for an actively maintained version of the tool. 6 | > If you are interested in maintaining your own version, feel free to fork it. Send your actively maintained fork to `alfeu.gds+jiraworklogtool@gmail.com` and I can link it in this README for future reference. 7 | 8 | # Active forks: 9 | 10 | | Fork | Chrome Web Store Listing | 11 | | ------------- | ------------- | 12 | | https://github.com/DiegoVAReis/jiraworkloghelper | [link](https://chromewebstore.google.com/detail/jira-worklog-helper/ghaalgapncgkjoecjdkcodieakmcdllc) | 13 | 14 | 15 | A simple Chrome Extension that allows adding worklogs in Jira easily. 16 | Logging your time in Jira doesn't need to be a pain anymore. If you already keep track of your tasks in a TODO list from a text file, then all you need to do is to adapt your list items to the below intuitive format: 17 | 18 | ``` 19 | -