From 5e34fb41b0bc8fbf1387552931dd1b1ac1289286 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/29] 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 9f72d95920168f4aecedca6031ab30e463abf4f6 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/29] 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 cd2c2f38b61f75ffed68946e7e16ac516f91e921 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/29] 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 93477f49c5355c0d590d444c575fa51bfbb58f1d 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/29] 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 1612b9a462ebfab5519200ff58e5423c8883c18a 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/29] 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 5c8bb8655dc71e3c018eaa96b6e1108c0e2be71e 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/29] 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 193ab8f0fd9d1e406fc8782ebb1b93c1d72c3a6c 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/29] 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 7d8ac6a6f8d595c9f883675c4db594b505a8cb46 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/29] 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 46b9a01d220298f7500e859e09cffa6c3c71b520 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/29] 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 07d64fefeecc41157a7178b419e9a946ec1072c9 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/29] 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 43831c59427f83faf15e7f0005b3a26922ce1be5 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/29] 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 9bc4e949a8d4e833481099d60a0597941d98b3b9 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/29] 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 7b3694b462f47fe3413c2807c87f105403ce02e0 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/29] 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 6a8a942f5184eadb9031bc9fc7ae1abf7df86935 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/29] 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 ff585d989a637956b709962913c67fea0fb54c08 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/29] 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 ccfd9daba30866f22f2ba9517a615bf4a825e10a 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/29] 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 0b1e4bf8a55dd30a94a2392bcb106f77127db031 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/29] 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 b99ab291c3381648c667826e2ce7fb004d82dc9a 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/29] 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 3ae71d4522a4c47de91b9900864a57c40aa0e595 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/29] 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 d64ac9f6309a676c6242c6a2c819c829556f5caa 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/29] 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 267b05ad9d519b23fb1a67819fd572e1358b0150 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/29] 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 209b2cbe59fab1acc48967992f05f47a11466e09 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/29] 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 59124412dad628af6cbc7ac51287a9f02483d8b6 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/29] 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 From 14a937569c5d11d48d7467b160540db58e83ae9b Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:00:40 +0200 Subject: [PATCH 24/29] Make comment better --- InfoLogger/public/Model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index 577afff1c7..3a0a197383 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -392,7 +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. + * Built from the model rather than the address bar, which is debounced. * @returns {string} - the shareable URL */ get shareableURL() { From 4c0de166ecae0d43165e3cdfa487e18524b86453 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:02:54 +0200 Subject: [PATCH 25/29] Match parameter of onFailure --- 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 b39be82621..58ade297cc 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -88,7 +88,7 @@ const copyURLButton = (url, showNotification) => h( id: 'url', className: '', style: { minWidth: '100px' }, - onFailure: ({ message }) => showNotification(`Could not copy URL: ${message}`, 'danger', 3000), + onFailure: ({ error }) => showNotification(`Could not copy URL: ${error}`, 'danger', 3000), }, 'Copy URL', ); From d99a0a3475c576b6bdb745e9b4c5718510deb834 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:03:24 +0200 Subject: [PATCH 26/29] Center copy URL button text --- InfoLogger/public/log/commandLogs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 58ade297cc..076490ef5b 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -88,6 +88,7 @@ const copyURLButton = (url, showNotification) => h( id: 'url', className: '', style: { minWidth: '100px' }, + contentClassName: 'justify-center', onFailure: ({ error }) => showNotification(`Could not copy URL: ${error}`, 'danger', 3000), }, 'Copy URL', From 79688c764019848281c76256ea80feaa530b2b61 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:10:23 +0200 Subject: [PATCH 27/29] Harden against querystring being null and resulting in a lonely ? --- InfoLogger/public/Model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index 3a0a197383..c69a98a930 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -397,7 +397,7 @@ export default class Model extends Observable { */ get shareableURL() { const url = this.router.getUrl(); - url.search = this.log.filter.queryString; + url.search = this.log.filter.queryString || ''; return url.href; } From 30f3fd8fa85edfc1c791481aa309474cffe41d29 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:57:37 +0200 Subject: [PATCH 28/29] Revert 4c0de166 --- 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 076490ef5b..84446a1594 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -89,7 +89,7 @@ const copyURLButton = (url, showNotification) => h( className: '', style: { minWidth: '100px' }, contentClassName: 'justify-center', - onFailure: ({ error }) => showNotification(`Could not copy URL: ${error}`, 'danger', 3000), + onFailure: ({ message }) => showNotification(`Could not copy URL: ${message}`, 'danger', 3000), }, 'Copy URL', ); From 6a483f964f67b1994fcec9b2c452035271d15084 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:10:13 +0200 Subject: [PATCH 29/29] Revert 79688c76 --- InfoLogger/public/Model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index c69a98a930..3a0a197383 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -397,7 +397,7 @@ export default class Model extends Observable { */ get shareableURL() { const url = this.router.getUrl(); - url.search = this.log.filter.queryString || ''; + url.search = this.log.filter.queryString; return url.href; }