From 3c64204cd45ff97b263a2d424e1b6d130ed6dc41 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 e82e00fd76d9075928836294d3a85641c4ac0740 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 5aff8e114d2b12e6b38aec4d5d34a680550db7e6 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 0597d31aa0602dbf8b7872f44a642c71092bca07 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 cbe406b469bdef539956f98df16248910b4659cf 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 8b2d035331ae0d75016b0b95d684d24d927de84e 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 39779a70b51583ec7f1f8b29f013286a8e90809d 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 c3f4d5073f86ceef6c289e5b3c458f3c8c2ebcb9 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 49696ec011567220ab8e2ec6f34fa9dcbab2a61c 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 6ed8287162335fc75a6b1455803837017791be62 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 2213d1019587c4581024348641a972fabde77ad5 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 ec4ab9b4cb93a2906a512e64d341c7263b249b5a 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 89fdd98e5f745282437f81bcf808da959c75d7d9 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 33130c7d10460b0e42532b4e3a2fa209c300c0dd 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 e63b36e19a019f184c0232aac9dc0dd8fb820267 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 de95067ac1f683d2a07e427c96f0553f84c39239 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 de172537357e3be8c9f221befaf5ebaffab73af8 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 e655b25f063fd74aed658a85ee249ea79e9441d5 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 16c659d65d5cb460268cd4d55022f8c533b4fd76 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 9108a66dfb2ebac7159711ef2d60eccdfb816ad3 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 083c9affb6fab095d10d65b33078979ed359eb8f 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 295c4b4c5b74e0ad95a05206fd82128bca0e7c08 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 1b34e6566b2d0e25bd975154aa7d9784d5007ad8 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