diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 06ca8932f..e62863248 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -20,6 +20,7 @@ import { h, iconMagnifyingGlass, iconPlus, iconMinus, + iconShare, } from '/js/src/index.js'; import { BUTTON } from '../constants/button-states.const.js'; import { MODE } from '../constants/mode.const.js'; @@ -67,6 +68,7 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), + h('', shareButton(model)), ]; /** @@ -242,6 +244,34 @@ const zoomButtonGroup = (zoom) => }, h('span', { style: 'font-size:0.8em' }, iconPlus())), ]); +const shareButton = (model) => + h('button.btn', { + onclick: () => copyLinkToShareCurrentView(model), + id: 'share-button', + title: 'Copy shareable link of current filters', + }, h('span', { style: 'font-size:0.9em' }, iconShare())); + +const copyLinkToShareCurrentView = (model) => { + if (!navigator.clipboard?.writeText) { + model.notification.show('Clipboard API is not available in this browser.', 'danger', 2000); + return; + } + const currentUrl = new URL(window.location.href); + const queryParams = new URLSearchParams(currentUrl.search); + const shareableLink = `${currentUrl.origin}${currentUrl.pathname}?${queryParams.toString()}`; + navigator.clipboard.writeText(shareableLink) + .then(() => { + model.notification.show( + 'Shareable link copied to clipboard.', + 'success', + 2000, + ); + }) + .catch(() => { + model.notification.show('Failed to copy shareable link to clipboard.', 'danger', 2000); + }); +}; + /** * Method to toggle states of the buttons(Query/Live) depending on the mode the tool is running on * @param {Model} model - root model of the application diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index 273b58edc..c11205f31 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -114,6 +114,7 @@ describe('InfoLogger', function () { require('./public/status-bar-mocha'); require('./public/zoom.mocha'); require('./public/log-context-menu-mocha'); + require('./public/share-mocha.js'); after(async () => { await browser.close(); diff --git a/InfoLogger/test/public/share-mocha.js b/InfoLogger/test/public/share-mocha.js new file mode 100644 index 000000000..e5ed8167e --- /dev/null +++ b/InfoLogger/test/public/share-mocha.js @@ -0,0 +1,130 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const assert = require('assert'); +const test = require('../mocha-index'); + +const SHARE_BUTTON = '#share-button'; +const NOTIFICATION = '.notification-content'; + +/** + * Install a fake clipboard which stores the written value in `window.__copiedValue` + * @param {Page} page - puppeteer page + * @returns {Promise} - resolves once the mock is installed + */ +const mockClipboard = (page) => page.evaluate(() => { + window.__copiedValue = ''; + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: (value) => { + window.__copiedValue = value; + return Promise.resolve(); + }, + }, + configurable: true, + }); +}); + +/** + * Wait for the notification to be displayed with the expected type and return its message + * @param {Page} page - puppeteer page + * @param {string} type - one of primary/success/warning/danger + * @returns {Promise} - the notification message + */ +const getNotification = async (page, type) => { + await page.waitForSelector(`${NOTIFICATION}.bg-${type}.notification-open`); + return (await page.$eval(NOTIFICATION, (el) => el.textContent)).trim(); +}; + +describe('Share button test-suite', () => { + let page = null; + + before(async () => { + ({ page } = test); + await page.goto(test.helpers.baseUrl, { waitUntil: 'networkidle0' }); + await page.waitForSelector(SHARE_BUTTON); + }); + + /* + * Only one notification is displayed at a time and it keeps its type class once hidden; + * dismiss it so the next test does not match the previous one's notification. + */ + afterEach(async () => { + if (await page.$(`${NOTIFICATION}.notification-open`)) { + await page.click(NOTIFICATION); + await page.waitForSelector(`${NOTIFICATION}.notification-close`); + } + }); + + it('should copy a shareable link to the clipboard', async () => { + await mockClipboard(page); + await page.click(SHARE_BUTTON); + + const notificationText = await getNotification(page, 'success'); + assert.strictEqual(notificationText, 'Shareable link copied to clipboard.'); + + const { copied, expected } = await page.evaluate(() => { + const url = new URL(window.location.href); + return { + copied: window.__copiedValue, + expected: `${url.origin}${url.pathname}?${new URLSearchParams(url.search).toString()}`, + }; + }); + assert.strictEqual(copied, expected); + }); + + it('should show a danger notification if the clipboard API is not available', async () => { + await page.evaluate(() => { + Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true }); + }); + await page.click(SHARE_BUTTON); + + const notificationText = await getNotification(page, 'danger'); + assert.strictEqual(notificationText, 'Clipboard API is not available in this browser.'); + }); + + it('should show a danger notification if the clipboard API fails', async () => { + await page.evaluate(() => { + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: () => Promise.reject(new Error('Random Error')), + }, + configurable: true, + }); + }); + await page.click(SHARE_BUTTON); + + const notificationText = await getNotification(page, 'danger'); + assert.strictEqual(notificationText, 'Failed to copy shareable link to clipboard.'); + }); + + it('should copy a URL that is shareable and contains the current filters', async () => { + await page.goto( + `${test.helpers.baseUrl}?q={"severity":{"in":"E F"}}`, + { waitUntil: 'networkidle0' }, + ); + await page.waitForSelector(SHARE_BUTTON); + await mockClipboard(page); + await page.click(SHARE_BUTTON); + + await getNotification(page, 'success'); + + const copied = await page.evaluate(() => window.__copiedValue); + assert.ok(copied.startsWith(test.helpers.baseUrl.replace(/\/$/, ''))); + assert.strictEqual( + decodeURIComponent(new URL(copied).searchParams.get('q')), + '{"severity":{"in":"E F"}}', + ); + }); +});