diff --git a/tests/unit/generic.test.js b/tests/unit/generic.test.js index 7cb3a64..ce88b31 100644 --- a/tests/unit/generic.test.js +++ b/tests/unit/generic.test.js @@ -123,21 +123,13 @@ describe('localStorage wrappers', () => { }); describe('createButtonWithCallback', () => { - test('callback works for button with style', () => { + test('creates button with callback and sets attributes', () => { const buttonCbMock = jest.fn(); - const button = generic.createButtonWithCallback('myclass', 'the text', 'border: 5px red;', buttonCbMock); - expect(buttonCbMock.mock.calls.length).toBe(0); - button.click(); - expect(buttonCbMock.mock.calls.length).toBe(1); - expect(button.style._values.border).toEqual('5px red'); - }); - - test('callback works for button without style', () => { - const buttonCbMock = jest.fn(); - const button = generic.createButtonWithCallback('myclass', 'the text', null, buttonCbMock); + const button = generic.createButtonWithCallback({ className: 'myclass' }, buttonCbMock); expect(buttonCbMock.mock.calls.length).toBe(0); button.click(); expect(buttonCbMock.mock.calls.length).toBe(1); + expect(button.className).toEqual('myclass'); }); }); diff --git a/tests/unit/search.test.js b/tests/unit/search.test.js index ff99a11..23da38d 100644 --- a/tests/unit/search.test.js +++ b/tests/unit/search.test.js @@ -218,28 +218,95 @@ describe('search method', () => { return search.searchHost('mih').then(() => { expect(global.setStatusText.mock.calls).toEqual([['__KEY_noResultsForMessage__ mih']]); expect(global.createButtonWithCallback.mock.calls).toEqual([ - ['login', '__KEY_createNewEntryButtonText__', null, global.switchToCreateNewDialog], + [ + { className: 'login', textContent: '__KEY_createNewEntryButtonText__' }, + global.switchToCreateNewDialog, + ], ]); }); }); - test('creates entry with three additional buttons for non empty response', () => { - expect.assertions(3); - global.sendNativeAppMessage.mockResolvedValueOnce(['some/entry']); - return search.searchHost('mih').then(() => { - expect(global.createButtonWithCallback.mock.calls).toEqual([ - ['login', 'some/entry', "background-image: url('icons/si-glyph-key-2.svg')", search._onEntryAction], - ['open', 'some/entry', null, expect.any(Function)], - ['copy', 'some/entry', null, expect.any(Function)], - ['details', 'some/entry', null, expect.any(Function)], - ]); + describe('additional search result buttons', () => { + test('are created for non empty response', () => { + expect.assertions(2); + global.sendNativeAppMessage.mockResolvedValueOnce(['some/entry']); + return search.searchHost('mih').then(() => { + expect(global.createButtonWithCallback.mock.calls).toEqual([ + [ + { + className: 'login', + textContent: 'some/entry', + style: "background-image: url('icons/si-glyph-key-2.svg')", + title: '__KEY_searchResultLoginTooltip__', + }, + search._onEntryAction, + ], + [ + { className: 'open', textContent: 'some/entry', title: '__KEY_searchResultOpenTooltip__' }, + expect.any(Function), + ], + [ + { className: 'copy', textContent: 'some/entry', title: '__KEY_searchResultCopyTooltip__' }, + expect.any(Function), + ], + [ + { + className: 'details', + textContent: 'some/entry', + title: '__KEY_searchResultDetailsTooltip__', + }, + expect.any(Function), + ], + ]); - expect(global.sendNativeAppMessage.mock.calls).toEqual([[{ host: 'mih', type: 'queryHost' }]]); - global.sendNativeAppMessage.mockClear(); - global.createButtonWithCallback.mock.calls[3][3]({ - target: { innerText: 'text' }, + expect(global.sendNativeAppMessage.mock.calls).toEqual([[{ host: 'mih', type: 'queryHost' }]]); + }); + }); + + test("'Open' tries to open in a new tab", () => { + expect.assertions(1); + global.sendNativeAppMessage.mockResolvedValueOnce(['some/entry']); + return search.searchHost('mih').then(() => { + global.sendNativeAppMessage.mockClear(); + global.createButtonWithCallback.mock.calls[1][1]({ + target: { innerText: 'text' }, + }); + expect(global.browser.runtime.sendMessage.mock.calls).toEqual([ + [{ entry: 'text', type: 'OPEN_TAB' }], + ]); + }); + }); + + test("'Copy' tries to copy to clipboard", () => { + expect.assertions(2); + global.sendNativeAppMessage.mockResolvedValueOnce(['some/entry']); + return search.searchHost('mih').then(() => { + global.sendNativeAppMessage.mockClear(); + const messagePromise = Promise.resolve({ password: '1234' }); + global.sendNativeAppMessage.mockImplementation(() => messagePromise); + + global.createButtonWithCallback.mock.calls[2][1]({ + target: { innerText: 'text' }, + }); + + return messagePromise.then(() => { + expect(global.copyToClipboard.mock.calls).toEqual([['1234']]); + jest.runAllTimers(); + expect(global.window.close.mock.calls.length).toBe(1); + }); + }); + }); + + test("'Details' tries to show details", () => { + expect.assertions(1); + global.sendNativeAppMessage.mockResolvedValueOnce(['some/entry']); + return search.searchHost('mih').then(() => { + global.sendNativeAppMessage.mockClear(); + global.createButtonWithCallback.mock.calls[3][1]({ + target: { innerText: 'text' }, + }); + expect(global.sendNativeAppMessage.mock.calls).toEqual([[{ entry: 'text', type: 'getData' }]]); }); - expect(global.sendNativeAppMessage.mock.calls).toEqual([[{ entry: 'text', type: 'getData' }]]); }); }); @@ -269,9 +336,12 @@ describe('search method', () => { global.sendNativeAppMessage.mockResolvedValueOnce(['some\\entry']); return search.searchHost('entry').then(() => { expect(global.createButtonWithCallback.mock.calls[0]).toEqual([ - 'login', - 'some\\entry', - "background-image: url('icons/si-glyph-key-2.svg')", + { + className: 'login', + textContent: 'some\\entry', + style: "background-image: url('icons/si-glyph-key-2.svg')", + title: '__KEY_searchResultLoginTooltip__', + }, search._onEntryAction, ]); }); @@ -284,9 +354,12 @@ describe('search method', () => { global.sendNativeAppMessage.mockResolvedValueOnce(['some\\entry']); return search.searchHost('entry').then(() => { expect(global.createButtonWithCallback.mock.calls[0]).toEqual([ - 'login', - 'some/entry', - "background-image: url('icons/si-glyph-key-2.svg')", + { + className: 'login', + textContent: 'some/entry', + style: "background-image: url('icons/si-glyph-key-2.svg')", + title: '__KEY_searchResultLoginTooltip__', + }, search._onEntryAction, ]); }); @@ -303,9 +376,12 @@ describe('search method', () => { global.sendNativeAppMessage.mockResolvedValueOnce(['some/entry']); return search.searchHost('mih').then(() => { expect(global.createButtonWithCallback.mock.calls[0]).toEqual([ - 'login', - 'some/entry', - "background-image: url('http://some.host/fav.ico')", + { + className: 'login', + textContent: 'some/entry', + style: "background-image: url('http://some.host/fav.ico')", + title: '__KEY_searchResultLoginTooltip__', + }, search._onEntryAction, ]); }); @@ -316,9 +392,12 @@ describe('search method', () => { global.sendNativeAppMessage.mockResolvedValueOnce(['some/entry']); return search.search('mih').then(() => { expect(global.createButtonWithCallback.mock.calls[0]).toEqual([ - 'login', - 'some/entry', - "background-image: url('icons/si-glyph-key-2.svg')", + { + className: 'login', + textContent: 'some/entry', + style: "background-image: url('icons/si-glyph-key-2.svg')", + title: '__KEY_searchResultLoginTooltip__', + }, search._onEntryAction, ]); }); diff --git a/web-extension/_locales/de/messages.json b/web-extension/_locales/de/messages.json index 99d5d4c..b1bc6da 100644 --- a/web-extension/_locales/de/messages.json +++ b/web-extension/_locales/de/messages.json @@ -35,6 +35,22 @@ "message": "Suchen", "description": "caption of search button" }, + "searchResultLoginTooltip": { + "message": "Anmelden", + "description": "tooltip for login button in search result" + }, + "searchResultOpenTooltip": { + "message": "In neuem Tab öffnen", + "description": "tooltip for open button in search result" + }, + "searchResultCopyTooltip": { + "message": "Kopieren", + "description": "tooltip for copy button in search result" + }, + "searchResultDetailsTooltip": { + "message": "Details anzeigen", + "description": "tooltip for details button in search result" + }, "createNameLabel": { "message": "Name", "description": "label for name" diff --git a/web-extension/_locales/en/messages.json b/web-extension/_locales/en/messages.json index 43459ef..cb20b42 100644 --- a/web-extension/_locales/en/messages.json +++ b/web-extension/_locales/en/messages.json @@ -35,6 +35,22 @@ "message": "Search", "description": "caption of search button" }, + "searchResultLoginTooltip": { + "message": "Login", + "description": "tooltip for login button in search result" + }, + "searchResultOpenTooltip": { + "message": "Open in new tab", + "description": "tooltip for open button in search result" + }, + "searchResultCopyTooltip": { + "message": "Copy", + "description": "tooltip for copy button in search result" + }, + "searchResultDetailsTooltip": { + "message": "Show details", + "description": "tooltip for details button in search result" + }, "createNameLabel": { "message": "Name", "description": "label for name" diff --git a/web-extension/generic.js b/web-extension/generic.js index 58c4694..d5ea9fc 100644 --- a/web-extension/generic.js +++ b/web-extension/generic.js @@ -84,13 +84,11 @@ function executeOnSetting(setting, trueCallback, falseCallback) { }, logError); } -function createButtonWithCallback(className, text, style, callback) { +function createButtonWithCallback(attributes, callback) { const element = document.createElement('button'); - element.className = className; - element.textContent = text; - if (style) { - element.style = style; - } + Object.keys(attributes).forEach(attribute => { + element[attribute] = attributes[attribute]; + }); element.addEventListener('click', callback); return element; } diff --git a/web-extension/search.js b/web-extension/search.js index a39690d..4327cca 100644 --- a/web-extension/search.js +++ b/web-extension/search.js @@ -99,34 +99,60 @@ function _faviconUrl() { } function _displaySearchResults(response, isHostQuery) { - const isWindows = window.navigator.userAgent.toLocaleLowerCase().includes('windows') - const results = document.getElementById('results'); - results.innerHTML = ''; - response.forEach(result => { + const isWindows = window.navigator.userAgent.toLocaleLowerCase().includes('windows'); + const results = document.getElementById('results'); + results.innerHTML = ''; + response.forEach(result => { // This is a workaround for gopass issue #1166 (windows only) - const item = isWindows ? result.replace(/\\/g, '/') : result + const item = isWindows ? result.replace(/\\/g, '/') : result; const entry = document.createElement('div'); entry.classList.add('entry'); + + entry.appendChild(_createSearchResultLoginButton(item, isHostQuery)); + entry.appendChild( + _createSimpleSearchResultButton('open', item, i18n.getMessage('searchResultOpenTooltip'), _onEntryOpen) + ); + entry.appendChild( + _createSimpleSearchResultButton('copy', item, i18n.getMessage('searchResultCopyTooltip'), _onEntryCopy) + ); entry.appendChild( - createButtonWithCallback( - 'login', + _createSimpleSearchResultButton( + 'details', item, - `background-image: url('${isHostQuery ? _faviconUrl() : 'icons/si-glyph-key-2.svg'}')`, - _onEntryAction + i18n.getMessage('searchResultDetailsTooltip'), + _onEntryDetails ) ); - entry.appendChild(createButtonWithCallback('open', item, null, event => _onEntryOpen(event.target))); - entry.appendChild(createButtonWithCallback('copy', item, null, event => _onEntryCopy(event.target))); - entry.appendChild(createButtonWithCallback('details', item, null, event => _onEntryDetails(event.target))); + results.appendChild(entry); }); } +const _createSearchResultLoginButton = (item, isHostQuery) => + createButtonWithCallback( + { + className: 'login', + textContent: item, + title: i18n.getMessage('searchResultLoginTooltip'), + style: `background-image: url('${isHostQuery ? _faviconUrl() : 'icons/si-glyph-key-2.svg'}')`, + }, + _onEntryAction + ); + +const _createSimpleSearchResultButton = (className, text, title, clickHandler) => + createButtonWithCallback({ className, title, textContent: text }, event => clickHandler(event.target)); + function _displayNoResults() { const results = document.getElementById('results'); setStatusText(i18n.getMessage('noResultsForMessage') + ' ' + searchTerm); results.appendChild( - createButtonWithCallback('login', i18n.getMessage('createNewEntryButtonText'), null, switchToCreateNewDialog) + createButtonWithCallback( + { + className: 'login', + textContent: i18n.getMessage('createNewEntryButtonText'), + }, + switchToCreateNewDialog + ) ); }