From 61dc05739b40a093f4f48cf416a49eb9b7c9d7f9 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:40:27 +0200 Subject: [PATCH 01/23] Add copy URL action to command logs Register the InfoLogger model with `StatefulComponent` so shared stateful UI components can render correctly. Add a `CopyToClipboardComponent` button in the command logs toolbar that copies the current filter query string as a URL. --- InfoLogger/public/index.js | 3 ++- InfoLogger/public/log/commandLogs.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/InfoLogger/public/index.js b/InfoLogger/public/index.js index 7b689a2b81..ec64790e86 100644 --- a/InfoLogger/public/index.js +++ b/InfoLogger/public/index.js @@ -19,13 +19,14 @@ sessionService.loadAndHideParameters(); window.sessionService = sessionService; // Import MVC -import { mount } from '/js/src/index.js'; +import { mount, StatefulComponent } from '/js/src/index.js'; import view from './view.js'; import Model from './Model.js'; // Start application const model = new Model(); const debug = true; // shows when redraw is done +StatefulComponent.useRenderer(model); // Register the model for the stateful components mount(document.body, view, model, debug); // Expose model to interact with it the browser's console diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 06ca8932f7..3df04cd0d7 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -20,6 +20,7 @@ import { h, iconMagnifyingGlass, iconPlus, iconMinus, + CopyToClipboardComponent, } from '/js/src/index.js'; import { BUTTON } from '../constants/button-states.const.js'; import { MODE } from '../constants/mode.const.js'; @@ -67,8 +68,22 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), + copyButtonOption(model.log.filter), + ]; +/** + * A button component that lets the user copy the url + * + * @param {Model} filterModel - filter model of the application + * @returns {Component} the copy button component + */ +const copyButtonOption = (filterModel) => h( + CopyToClipboardComponent, + { value: filterModel.queryString, id: 'url', className: 'button.btn', style: { minWidth: '100px' } }, + 'Copy URL', +); + /** * Group of buttons for switching between Query and Live modes. * @param {Model} model - root model of the application From f35807ce9b4af834346faad9a0ec06e8bfb39cbd Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:11:06 +0200 Subject: [PATCH 02/23] Fix copy url value Switch the command logs copy action to use `location.href`. --- InfoLogger/public/log/commandLogs.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 3df04cd0d7..b5a4f66098 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -68,19 +68,18 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), - copyButtonOption(model.log.filter), + copyButtonOption(), ]; /** * A button component that lets the user copy the url * - * @param {Model} filterModel - filter model of the application * @returns {Component} the copy button component */ -const copyButtonOption = (filterModel) => h( +const copyButtonOption = () => h( CopyToClipboardComponent, - { value: filterModel.queryString, id: 'url', className: 'button.btn', style: { minWidth: '100px' } }, + { value: location.href, id: 'url', className: 'button.btn', style: { minWidth: '100px' } }, 'Copy URL', ); From 9893f94e6a0d103687c1ddea9a41af30444a7e5b Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:33:14 +0200 Subject: [PATCH 03/23] Should copy the non-debounced version of URL --- InfoLogger/public/log/commandLogs.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index b5a4f66098..21b8f67028 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -68,18 +68,25 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), - copyButtonOption(), + copyButtonOption(model.log.filter.queryString), ]; /** * A button component that lets the user copy the url * + * @param {string} queryString - the query string to be appended to the URL * @returns {Component} the copy button component */ -const copyButtonOption = () => h( +const copyButtonOption = (queryString) => h( CopyToClipboardComponent, - { value: location.href, id: 'url', className: 'button.btn', style: { minWidth: '100px' } }, + { + // Copy the non-debounced URL with the current query string + value: `${location.origin}${location.pathname}${queryString}`, + id: 'url', + className: 'button.btn', + style: { minWidth: '100px' }, + }, 'Copy URL', ); From 31c1beba284037a5e8bb4eab18da4e5a64e56719 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:34:16 +0200 Subject: [PATCH 04/23] Add tests for copy URL button Add a new test suite for the copy URL button. The new tests verify the button label and confirm that clicking it copies a URL containing the encoded active filter query. --- InfoLogger/test/mocha-index.js | 1 + InfoLogger/test/public/copy-url-btn-mocha.js | 49 ++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 InfoLogger/test/public/copy-url-btn-mocha.js diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index b8e50c7373..c94df8820d 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -115,6 +115,7 @@ describe('InfoLogger', function () { require('./public/status-bar-mocha'); require('./public/zoom.mocha'); require('./public/log-context-menu-mocha'); + require('./public/copy-url-btn-mocha'); after(async () => { await browser.close(); diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js new file mode 100644 index 0000000000..14bc5de832 --- /dev/null +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -0,0 +1,49 @@ +/** + * @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'); + +describe('Copy URL button test-suite', async () => { + let baseUrl = null; + let page = null; + + before(async () => { + ({ helpers: { baseUrl }, page } = test); + await page.browser().defaultBrowserContext().setPermission( + new URL(baseUrl).origin, + { permission: { name: 'clipboard-read' }, state: 'granted' }, + { permission: { name: 'clipboard-write' }, state: 'granted' }, + ); + await page.goto(baseUrl, { waitUntil: 'networkidle0' }); + }); + + it('should display the button with the correct label', async () => { + const button = await page.$('#copy-url'); + const label = await page.evaluate((el) => el.textContent, button); + assert.strictEqual(label, 'Copy URL'); + }); + + it('should copy a URL carrying the active filter', async () => { + await page.evaluate(() => { + window.model.log.filter.setCriteria('message', 'match', 'needle'); + window.model.notify(); + }); + await page.click('#copy-url'); + const copiedText = await page.evaluate(() => navigator.clipboard.readText()); + const expectedUrl = `${baseUrl}?q=%7B%22message%22%3A%7B%22match%22` + + '%3A%22needle%22%7D%2C%22severity%22%3A%7B%22in%22%3A%22I%20W%20E%20F%22%7D%7D'; + assert.strictEqual(copiedText, expectedUrl); + }); +}); From 93b3a28c29622de64adf90076ffa25c34ec4f85b Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:11:10 +0200 Subject: [PATCH 05/23] Rename copy button --- InfoLogger/public/log/commandLogs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 21b8f67028..32f32a9ecd 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -68,7 +68,7 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), - copyButtonOption(model.log.filter.queryString), + copyURLButton(model.log.filter.queryString), ]; @@ -78,7 +78,7 @@ export const commandLogs = (model) => [ * @param {string} queryString - the query string to be appended to the URL * @returns {Component} the copy button component */ -const copyButtonOption = (queryString) => h( +const copyURLButton = (queryString) => h( CopyToClipboardComponent, { // Copy the non-debounced URL with the current query string From c919cf3ab33c8b65df45a32b80393b69095a372c Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:28:55 +0200 Subject: [PATCH 06/23] Remove references to location in a view Switch the log view Copy URL button to consume a new `LogFilter.filterURL` getter instead of rebuilding from a query string in the button component. --- InfoLogger/public/log/commandLogs.js | 9 ++++----- InfoLogger/public/logFilter/LogFilter.js | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 32f32a9ecd..866b1bc7c5 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -68,21 +68,20 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), - copyURLButton(model.log.filter.queryString), - + copyURLButton(model.log.filter.filterURL), ]; /** * A button component that lets the user copy the url * - * @param {string} queryString - the query string to be appended to the URL + * @param {string} url - the url string to be appended to the URL * @returns {Component} the copy button component */ -const copyURLButton = (queryString) => h( +const copyURLButton = (url) => h( CopyToClipboardComponent, { // Copy the non-debounced URL with the current query string - value: `${location.origin}${location.pathname}${queryString}`, + value: url, id: 'url', className: 'button.btn', style: { minWidth: '100px' }, diff --git a/InfoLogger/public/logFilter/LogFilter.js b/InfoLogger/public/logFilter/LogFilter.js index 61e0bfe7e0..a1e52d2a67 100644 --- a/InfoLogger/public/logFilter/LogFilter.js +++ b/InfoLogger/public/logFilter/LogFilter.js @@ -159,6 +159,10 @@ export default class LogFilter extends Observable { return buildUrl('?', { q: JSON.stringify(this.toObject()) }); } + get filterURL() { + return `${location.origin}${location.pathname}${this.queryString}`; + } + /** * Set criterias according to object passed as argument * @param {object} criterias - object with criterias to be set From b6f200c2acc59f4daf3aadc549c096334bdb6dc4 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:29:59 +0200 Subject: [PATCH 07/23] Change copyURL button to reference correct attr --- InfoLogger/public/log/commandLogs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 866b1bc7c5..7e3c4356ff 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -83,7 +83,7 @@ const copyURLButton = (url) => h( // Copy the non-debounced URL with the current query string value: url, id: 'url', - className: 'button.btn', + classes: '', style: { minWidth: '100px' }, }, 'Copy URL', From 216f5c39f2062b1f06d3264ee2e6cca073273fd6 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:13:28 +0200 Subject: [PATCH 08/23] Reset clipboard test permissions and improve JSDOC --- InfoLogger/test/public/copy-url-btn-mocha.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index 14bc5de832..c838d5ef5c 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -29,6 +29,11 @@ describe('Copy URL button test-suite', async () => { await page.goto(baseUrl, { waitUntil: 'networkidle0' }); }); + after(async () => { + await page.browser().defaultBrowserContext().clearPermissionOverrides(); + await page.goto(baseUrl, { waitUntil: 'networkidle0' }); + }); + it('should display the button with the correct label', async () => { const button = await page.$('#copy-url'); const label = await page.evaluate((el) => el.textContent, button); From ab89245083343263ebdb9714c54ad172aa27c87a Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:26:20 +0200 Subject: [PATCH 09/23] Remove window location calls from log filter model Move the shareable URL logic to the main model from the more specific log filter model. Build URL using `queryRouter.getUrl` and replacing the search. --- InfoLogger/public/Model.js | 10 ++++++++++ InfoLogger/public/log/commandLogs.js | 2 +- InfoLogger/public/logFilter/LogFilter.js | 4 ---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index fbd4d5cb32..4bd33d59dd 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -390,6 +390,16 @@ export default class Model extends Observable { this.router.go(this.log.filter.queryString, true, true); } + /** + * Get the shareable URL with the current filter query string + * @returns {string} - the shareable URL + */ + get shareableURL() { + const url = this.router.getUrl(); + url.search = this.log.filter.queryString; + return url.href; + } + /** * Toggle inspector on the right */ diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 7e3c4356ff..dbd30ff586 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -68,7 +68,7 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), - copyURLButton(model.log.filter.filterURL), + copyURLButton(model.shareableURL), ]; /** diff --git a/InfoLogger/public/logFilter/LogFilter.js b/InfoLogger/public/logFilter/LogFilter.js index a1e52d2a67..61e0bfe7e0 100644 --- a/InfoLogger/public/logFilter/LogFilter.js +++ b/InfoLogger/public/logFilter/LogFilter.js @@ -159,10 +159,6 @@ export default class LogFilter extends Observable { return buildUrl('?', { q: JSON.stringify(this.toObject()) }); } - get filterURL() { - return `${location.origin}${location.pathname}${this.queryString}`; - } - /** * Set criterias according to object passed as argument * @param {object} criterias - object with criterias to be set From 47c4b30c046457b3ad49b1ba26b8dc98dc2ce035 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:29:15 +0200 Subject: [PATCH 10/23] Share render wait helper across UI tests --- InfoLogger/test/public/context-menu-test-utils.js | 12 ++---------- InfoLogger/test/public/copy-url-btn-mocha.js | 3 +++ InfoLogger/test/utils/utils.js | 11 +++++++++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/InfoLogger/test/public/context-menu-test-utils.js b/InfoLogger/test/public/context-menu-test-utils.js index 26dd310423..e66d663257 100644 --- a/InfoLogger/test/public/context-menu-test-utils.js +++ b/InfoLogger/test/public/context-menu-test-utils.js @@ -12,22 +12,14 @@ * or submit itself to any jurisdiction. */ +const { waitForNextRender } = require('../utils/utils.js'); + const isContextMenuOpen = async (page) => await page.evaluate(() => window.model.log.contextMenu.isOpen); const getMenuActionLabels = async (page) => page.evaluate(() => Array.from(document.querySelectorAll('.cell-context-menu-item .ph2.w-100')) .map((el) => el.textContent.trim())); -/* - * A stale menu from a previous test can already satisfy a waitForSelector check - * before the pending redraw (reflecting the new state) has actually run. - * Waiting for two animation frames guarantees the debounced redraw has fired - * at least once since the mutation. - */ -const waitForNextRender = (page) => page.evaluate(() => new Promise((resolve) => { - requestAnimationFrame(() => requestAnimationFrame(resolve)); -})); - const openContextMenu = async (page, field, value, x, y) => { await page.evaluate((field, value, x, y) => { window.model.log.contextMenu.show(field, value, x, y); diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index c838d5ef5c..fac6251eb4 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -15,6 +15,8 @@ const assert = require('assert'); const test = require('../mocha-index'); +const { waitForNextRender } = require('../utils/utils.js'); + describe('Copy URL button test-suite', async () => { let baseUrl = null; let page = null; @@ -45,6 +47,7 @@ describe('Copy URL button test-suite', async () => { window.model.log.filter.setCriteria('message', 'match', 'needle'); window.model.notify(); }); + await waitForNextRender(page); await page.click('#copy-url'); const copiedText = await page.evaluate(() => navigator.clipboard.readText()); const expectedUrl = `${baseUrl}?q=%7B%22message%22%3A%7B%22match%22` diff --git a/InfoLogger/test/utils/utils.js b/InfoLogger/test/utils/utils.js index d4a21ae580..d2addae2ba 100644 --- a/InfoLogger/test/utils/utils.js +++ b/InfoLogger/test/utils/utils.js @@ -44,7 +44,18 @@ async function waitForTextInElement(page, selector, text) { ); } +/* + * A stale element from a previous test can already satisfy a waitForSelector check + * before the pending redraw (reflecting the new state) has actually run. + * Waiting for two animation frames guarantees the redraw has fired at least once + * since the mutation. + */ +const waitForNextRender = (page) => page.evaluate(() => new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(resolve)); +})); + module.exports = { injectLogs, waitForTextInElement, + waitForNextRender, }; From f7e16002fe6a6417dbc4bc398c1c03484f57a9de Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:48:14 +0200 Subject: [PATCH 11/23] Fix URL copy button prop name --- InfoLogger/public/log/commandLogs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index dbd30ff586..73326ca3a3 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -83,7 +83,7 @@ const copyURLButton = (url) => h( // Copy the non-debounced URL with the current query string value: url, id: 'url', - classes: '', + className: '', style: { minWidth: '100px' }, }, 'Copy URL', From 01a4922ce2744252aa37178317c7512ceaba84bc Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:56:58 +0200 Subject: [PATCH 12/23] Notify on Copy URL clipboard failures Wires an `onFailure` handler so clipboard errors are surfaced to users as a danger notification. Adds a test that simulates a clipboard rejection and verifies the expected notification state, type, and message. --- InfoLogger/public/log/commandLogs.js | 5 +++-- InfoLogger/test/public/copy-url-btn-mocha.js | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 73326ca3a3..1fa7484f7e 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -68,7 +68,7 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), - copyURLButton(model.shareableURL), + copyURLButton(model.shareableURL, model.notification), ]; /** @@ -77,7 +77,7 @@ export const commandLogs = (model) => [ * @param {string} url - the url string to be appended to the URL * @returns {Component} the copy button component */ -const copyURLButton = (url) => h( +const copyURLButton = (url, notification) => h( CopyToClipboardComponent, { // Copy the non-debounced URL with the current query string @@ -85,6 +85,7 @@ const copyURLButton = (url) => h( id: 'url', className: '', style: { minWidth: '100px' }, + onFailure: ({ message }) => notification.show(`Could not copy URL: ${message}`, 'danger', 3000), }, 'Copy URL', ); diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index fac6251eb4..0a116ab7a3 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -54,4 +54,16 @@ describe('Copy URL button test-suite', async () => { + '%3A%22needle%22%7D%2C%22severity%22%3A%7B%22in%22%3A%22I%20W%20E%20F%22%7D%7D'; assert.strictEqual(copiedText, expectedUrl); }); + + it('should display a notification on copy failure', async () => { + await page.evaluate(() => { + navigator.clipboard.writeText = () => Promise.reject(new Error('Simulated copy failure')); + }); + + await page.click('#copy-url'); + + await page.waitForFunction('window.model.notification.state === \'shown\''); + await page.waitForFunction('window.model.notification.type === \'danger\''); + await page.waitForFunction('window.model.notification.message === "Could not copy URL: Simulated copy failure"'); + }); }); From 047b44193dbacf6355139feae3b5611cda0e7877 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:23:52 +0200 Subject: [PATCH 13/23] Stabilize copy URL failure notification test --- InfoLogger/test/public/copy-url-btn-mocha.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index 0a116ab7a3..06b1b891b9 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -62,8 +62,9 @@ describe('Copy URL button test-suite', async () => { await page.click('#copy-url'); - await page.waitForFunction('window.model.notification.state === \'shown\''); - await page.waitForFunction('window.model.notification.type === \'danger\''); - await page.waitForFunction('window.model.notification.message === "Could not copy URL: Simulated copy failure"'); + const notification = await page.evaluate(() => window.model.notification); + assert.strictEqual(notification.state, 'shown'); + assert.strictEqual(notification.type, 'danger'); + assert.strictEqual(notification.message, 'Could not copy URL: Simulated copy failure'); }); }); From 4117f4f899a5698e8ceee4e28c28e6c7da5f2d8a Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:32:03 +0200 Subject: [PATCH 14/23] Trying to stabilise notifcation test --- InfoLogger/test/public/copy-url-btn-mocha.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index 06b1b891b9..d88ae25900 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -57,14 +57,15 @@ describe('Copy URL button test-suite', async () => { it('should display a notification on copy failure', async () => { await page.evaluate(() => { + model.notification.hide(); navigator.clipboard.writeText = () => Promise.reject(new Error('Simulated copy failure')); }); + await waitForNextRender(page); await page.click('#copy-url'); - const notification = await page.evaluate(() => window.model.notification); - assert.strictEqual(notification.state, 'shown'); - assert.strictEqual(notification.type, 'danger'); - assert.strictEqual(notification.message, 'Could not copy URL: Simulated copy failure'); + await page.waitForFunction('model.notification.state === \'shown\''); + await page.waitForFunction('model.notification.type === \'danger\''); + await page.waitForFunction('model.notification.message === "Could not copy URL: Simulated copy failure"'); }); }); From 1b9b61629055972c6d199081fe0b5201e9625d51 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:44:23 +0200 Subject: [PATCH 15/23] Fix unreliable test --- InfoLogger/test/public/copy-url-btn-mocha.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index d88ae25900..09bd2200ca 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -64,8 +64,15 @@ describe('Copy URL button test-suite', async () => { await page.click('#copy-url'); - await page.waitForFunction('model.notification.state === \'shown\''); - await page.waitForFunction('model.notification.type === \'danger\''); - await page.waitForFunction('model.notification.message === "Could not copy URL: Simulated copy failure"'); + await page.waitForFunction(() => window.model.notification.state === 'shown'); + const notification = await page.evaluate(() => ({ + message: window.model.notification.message, + type: window.model.notification.type, + })); + + assert.strictEqual(notification.message, 'Could not copy URL: Simulated copy failure'); + assert.strictEqual(notification.type, 'danger'); + + await page.evaluate(() => delete navigator.clipboard.writeText); }); }); From f6a41dd9cfe952f247b45640be07b5ae245c48a8 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:47:12 +0200 Subject: [PATCH 16/23] Use another way to mock erroneous clipboard --- InfoLogger/test/public/copy-url-btn-mocha.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index 09bd2200ca..b301b27ca3 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -57,10 +57,13 @@ describe('Copy URL button test-suite', async () => { it('should display a notification on copy failure', async () => { await page.evaluate(() => { - model.notification.hide(); - navigator.clipboard.writeText = () => Promise.reject(new Error('Simulated copy failure')); + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: () => Promise.reject(new Error('Clipboard access denied')), + }, + configurable: true, + }); }); - await waitForNextRender(page); await page.click('#copy-url'); From 507711489f63f7d5f5828bec62d68c82ca382897 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:47:52 +0200 Subject: [PATCH 17/23] Fix wrong assert text --- InfoLogger/test/public/copy-url-btn-mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index b301b27ca3..52954f7bd7 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -59,7 +59,7 @@ describe('Copy URL button test-suite', async () => { await page.evaluate(() => { Object.defineProperty(navigator, 'clipboard', { value: { - writeText: () => Promise.reject(new Error('Clipboard access denied')), + writeText: () => Promise.reject(new Error('Simulated copy failure')), }, configurable: true, }); From d61abd9e3654236fda19483ab0ff9a6b53b8938c Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:54:33 +0200 Subject: [PATCH 18/23] Stabilise test again --- InfoLogger/test/public/copy-url-btn-mocha.js | 1 + 1 file changed, 1 insertion(+) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index 52954f7bd7..29d2c4c44d 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -57,6 +57,7 @@ describe('Copy URL button test-suite', async () => { it('should display a notification on copy failure', async () => { await page.evaluate(() => { + window.model.notification.hide(); Object.defineProperty(navigator, 'clipboard', { value: { writeText: () => Promise.reject(new Error('Simulated copy failure')), From 2964ce4eb8d7bc9495ced3dec6d8e25f270d241b Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:58:46 +0200 Subject: [PATCH 19/23] Use global model in copy URL mocha test --- InfoLogger/test/public/copy-url-btn-mocha.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index 29d2c4c44d..c69ed038cf 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -44,8 +44,8 @@ describe('Copy URL button test-suite', async () => { it('should copy a URL carrying the active filter', async () => { await page.evaluate(() => { - window.model.log.filter.setCriteria('message', 'match', 'needle'); - window.model.notify(); + model.log.filter.setCriteria('message', 'match', 'needle'); + model.notify(); }); await waitForNextRender(page); await page.click('#copy-url'); @@ -57,7 +57,7 @@ describe('Copy URL button test-suite', async () => { it('should display a notification on copy failure', async () => { await page.evaluate(() => { - window.model.notification.hide(); + model.notification.hide(); Object.defineProperty(navigator, 'clipboard', { value: { writeText: () => Promise.reject(new Error('Simulated copy failure')), @@ -68,10 +68,10 @@ describe('Copy URL button test-suite', async () => { await page.click('#copy-url'); - await page.waitForFunction(() => window.model.notification.state === 'shown'); + await page.waitForFunction(() => model.notification.state === 'shown'); const notification = await page.evaluate(() => ({ - message: window.model.notification.message, - type: window.model.notification.type, + message: model.notification.message, + type: model.notification.type, })); assert.strictEqual(notification.message, 'Could not copy URL: Simulated copy failure'); From 3178b9c8504710283bf1b9e2a4bbcf8adca4457f Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:06:12 +0200 Subject: [PATCH 20/23] Update copy URL button test Adjust the copy URL button mocha test to use the current log filtering API and assert copy failures through the rendered danger notification instead of reading notification state directly. --- InfoLogger/test/public/copy-url-btn-mocha.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/InfoLogger/test/public/copy-url-btn-mocha.js b/InfoLogger/test/public/copy-url-btn-mocha.js index c69ed038cf..6630767b20 100644 --- a/InfoLogger/test/public/copy-url-btn-mocha.js +++ b/InfoLogger/test/public/copy-url-btn-mocha.js @@ -44,7 +44,7 @@ describe('Copy URL button test-suite', async () => { it('should copy a URL carrying the active filter', async () => { await page.evaluate(() => { - model.log.filter.setCriteria('message', 'match', 'needle'); + model.log.setCriteria('message', 'match', 'needle'); model.notify(); }); await waitForNextRender(page); @@ -68,15 +68,6 @@ describe('Copy URL button test-suite', async () => { await page.click('#copy-url'); - await page.waitForFunction(() => model.notification.state === 'shown'); - const notification = await page.evaluate(() => ({ - message: model.notification.message, - type: model.notification.type, - })); - - assert.strictEqual(notification.message, 'Could not copy URL: Simulated copy failure'); - assert.strictEqual(notification.type, 'danger'); - - await page.evaluate(() => delete navigator.clipboard.writeText); + await page.waitForSelector('.notification-content.bg-danger.notification-open'); }); }); From f6302da22f875fc5359aaa7914bc48157acaaaed Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:06:36 +0200 Subject: [PATCH 21/23] Document better the shareable URL getter --- InfoLogger/public/Model.js | 1 + 1 file changed, 1 insertion(+) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index 4bd33d59dd..577afff1c7 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -392,6 +392,7 @@ export default class Model extends Observable { /** * Get the shareable URL with the current filter query string + * Built from the model rather than the address bar, which only updates on a 500 ms rate limit. * @returns {string} - the shareable URL */ get shareableURL() { From 199be7dda9924e8ac96b768a505eaa3baf0caa61 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:07:45 +0200 Subject: [PATCH 22/23] Improve the URL notification callback Pass the notification `show` method as a bound callback when rendering the copy URL button. --- InfoLogger/public/log/commandLogs.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 1fa7484f7e..e885f376cb 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -68,24 +68,28 @@ export const commandLogs = (model) => [ ]), h('', downloadButtonGroup(model.log)), h('', zoomButtonGroup(model.zoom)), - copyURLButton(model.shareableURL, model.notification), + copyURLButton( + model.shareableURL, + (message, type, duration) => model.notification.show(message, type, duration), + ), ]; /** * A button component that lets the user copy the url * - * @param {string} url - the url string to be appended to the URL + * @param {string} url - the URL to be copied to the clipboard + * @param {(message: string, type: string, duration: number) => void} showNotification - + * function to show notification to the user * @returns {Component} the copy button component */ -const copyURLButton = (url, notification) => h( +const copyURLButton = (url, showNotification) => h( CopyToClipboardComponent, { - // Copy the non-debounced URL with the current query string value: url, id: 'url', className: '', style: { minWidth: '100px' }, - onFailure: ({ message }) => notification.show(`Could not copy URL: ${message}`, 'danger', 3000), + onFailure: ({ message }) => showNotification(`Could not copy URL: ${message}`, 'danger', 3000), }, 'Copy URL', ); From 63770cf59a50555d3c97a95f5dfdac3d1e2aca20 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:10:12 +0200 Subject: [PATCH 23/23] Fix JSDoc --- InfoLogger/public/log/commandLogs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index e885f376cb..b39be82621 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -76,7 +76,6 @@ export const commandLogs = (model) => [ /** * A button component that lets the user copy the url - * * @param {string} url - the URL to be copied to the clipboard * @param {(message: string, type: string, duration: number) => void} showNotification - * function to show notification to the user