From 5fcb1488ae79467b3c3d79b55ddf24ac569deb54 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:47:17 +0200 Subject: [PATCH 01/24] Keep autoscroll enabled when list shrinks on switch to live mode The table scroll handler treated any decrease in scrollTop as the user scrolling up however this caught also just when the content shrunk which happens on switch from a full query mode to an initially empty live mode. Check now also if at the bottom of the page and if we still are then don't disableAutoScroll. --- InfoLogger/public/log/tableLogsContent.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index e0a207a48..c23b6f456 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -210,8 +210,12 @@ const tableContainerHooks = (model) => ({ const container = vnode.dom; const { height } = container.getBoundingClientRect(); const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling - if (container.scrollTop < model.log.scrollTop) { - model.log.disableAutoScroll(); // stop auto-scrolling if user scroll sup + // when content shrinks the browser clamps scrollTop to the new maximum + // that decrease is not the user scrolling up + // within 1px (to catch fractional heights) counts as the bottom + const isAtBottom = container.scrollHeight - container.clientHeight - scrollTop <= 1; + if (scrollTop < model.log.scrollTop && !isAtBottom) { + model.log.disableAutoScroll(); } model.log.setScrollTop(scrollTop, height); }; From ef4e85f0a672835f95e7231377723efd05531ad0 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:47:52 +0200 Subject: [PATCH 02/24] Add test to catch disabling of autoscroll on switch to live mode --- InfoLogger/public/log/commandLogs.js | 1 + InfoLogger/test/public/live-mode-mocha.js | 26 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 06ca8932f..805fccfe1 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -140,6 +140,7 @@ const liveButton = (model, frameworkInfo) => { const title = isLiveModeReady ? 'Stream logs with filtering' : 'Live service not configured'; return h('button.btn.bold', { + id: 'live-button', title, disabled: !isLiveModeReady || queryResult.isLoading(), className: !isLiveModeReady ? 'loading' : liveButtonType, diff --git a/InfoLogger/test/public/live-mode-mocha.js b/InfoLogger/test/public/live-mode-mocha.js index 271786f8e..881b4cfa3 100644 --- a/InfoLogger/test/public/live-mode-mocha.js +++ b/InfoLogger/test/public/live-mode-mocha.js @@ -14,6 +14,7 @@ const assert = require('assert'); const test = require('../mocha-index'); +const { injectLogs } = require('../utils/utils'); const isFieldEmpty = (value) => value === undefined || value === null || value === ''; @@ -24,6 +25,10 @@ describe('Live Mode test-suite', async () => { ({ helpers: { baseUrl }, page } = test); }); + after(async () => { + await page.evaluate(() => model.log.liveStop('Query')); + }); + it('should successfully go to homepage with predefined filters', async () => { await page.goto(baseUrl, { waitUntil: 'networkidle0' }); const location = await page.evaluate(() => window.location); @@ -231,4 +236,25 @@ describe('Live Mode test-suite', async () => { assert.deepStrictEqual(activeMode, 'Query'); }); + + it('should not disable autoscroll when switching from query table to live mode', async () => { + await page.evaluate(() => model.log.liveStop('Query')); + + // ensure table has more rows than fit on screen and is scrolled to the bottom + // as the test case is where scrollTop clamps back to 0 + await injectLogs(page, Array.from({ length: 200 }, (_, i) => ({ + severity: 'I', + message: `info log ${i}`, + timestamp: Date.now() + i, + }))); + + await page.evaluate(() => model.log.goToLastItem()); + await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); + + await page.click('#live-button'); + await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); + + const autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + }); }); From 976c684811ca34afd45af2b99e30a70b38dc035a Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:38:36 +0200 Subject: [PATCH 03/24] Clarify scroll handling --- InfoLogger/public/log/tableLogsContent.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index c23b6f456..ee50f5b2f 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -210,9 +210,12 @@ const tableContainerHooks = (model) => ({ const container = vnode.dom; const { height } = container.getBoundingClientRect(); const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling - // when content shrinks the browser clamps scrollTop to the new maximum - // that decrease is not the user scrolling up - // within 1px (to catch fractional heights) counts as the bottom + + // LogsTable is emptied when switching to Live mode or clearing it. + // This causes a scroll event to be triggered as scrollTop is reset to 0. + // If scrollTop is smaller than before this is usually enough to tell if the user has scrolled up. + // But we also need to check if the user is not at the bottom to avoid treating the above cases as scrolls up. + // <=1 to catch fractional heights. const isAtBottom = container.scrollHeight - container.clientHeight - scrollTop <= 1; if (scrollTop < model.log.scrollTop && !isAtBottom) { model.log.disableAutoScroll(); From 0bed3c908eb481888a9a57302952f50579715b9c Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:42:32 +0200 Subject: [PATCH 04/24] Add logs table autoscroll regression tests Introduces a suite to cover autoscroll behaviour when users scroll, switch modes, and clear logs in both query and live mode. --- InfoLogger/public/log/commandLogs.js | 2 +- InfoLogger/test/mocha-index.js | 17 +-- InfoLogger/test/public/live-mode-mocha.js | 22 ---- InfoLogger/test/public/logs-table-mocha.js | 132 +++++++++++++++++++++ 4 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 InfoLogger/test/public/logs-table-mocha.js diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 805fccfe1..94af5c3dc 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -37,7 +37,7 @@ let liveButtonIcon = iconMediaPlay(); export const commandLogs = (model) => [ userActionsDropdown(model), h('', interactionModesGroupButton(model)), - h('', h('button.btn', { onclick: () => model.log.empty(), style: 'font-weight: bold' }, 'Clear')), + h('', h('button.btn', { id: 'clear-button', onclick: () => model.log.empty(), style: 'font-weight: bold' }, 'Clear')), h('.btn-group', [ h('button.btn', { disabled: !model.log.list.length, diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index b8e50c737..118eb97b5 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -107,14 +107,15 @@ describe('InfoLogger', function () { assert.deepStrictEqual(search, '?q={"severity":{"in":"I W E F"}}'); }); - require('./public/user-actions-mocha'); - require('./public/log-filter-actions-mocha'); - require('./public/log-filter-url-mocha'); - require('./public/live-mode-mocha'); - require('./public/query-mode-mocha'); - require('./public/status-bar-mocha'); - require('./public/zoom.mocha'); - require('./public/log-context-menu-mocha'); + // require('./public/user-actions-mocha'); + // require('./public/log-filter-actions-mocha'); + // require('./public/log-filter-url-mocha'); + // require('./public/live-mode-mocha'); + // require('./public/query-mode-mocha'); + // require('./public/status-bar-mocha'); + // require('./public/zoom.mocha'); + // require('./public/log-context-menu-mocha'); + require('./public/logs-table-mocha'); after(async () => { await browser.close(); diff --git a/InfoLogger/test/public/live-mode-mocha.js b/InfoLogger/test/public/live-mode-mocha.js index 881b4cfa3..7664dce16 100644 --- a/InfoLogger/test/public/live-mode-mocha.js +++ b/InfoLogger/test/public/live-mode-mocha.js @@ -14,7 +14,6 @@ const assert = require('assert'); const test = require('../mocha-index'); -const { injectLogs } = require('../utils/utils'); const isFieldEmpty = (value) => value === undefined || value === null || value === ''; @@ -236,25 +235,4 @@ describe('Live Mode test-suite', async () => { assert.deepStrictEqual(activeMode, 'Query'); }); - - it('should not disable autoscroll when switching from query table to live mode', async () => { - await page.evaluate(() => model.log.liveStop('Query')); - - // ensure table has more rows than fit on screen and is scrolled to the bottom - // as the test case is where scrollTop clamps back to 0 - await injectLogs(page, Array.from({ length: 200 }, (_, i) => ({ - severity: 'I', - message: `info log ${i}`, - timestamp: Date.now() + i, - }))); - - await page.evaluate(() => model.log.goToLastItem()); - await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); - - await page.click('#live-button'); - await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); - - const autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); - }); }); diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js new file mode 100644 index 000000000..dd2671021 --- /dev/null +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -0,0 +1,132 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const assert = require('assert'); +const test = require('../mocha-index'); +const { injectLogs } = require('../utils/utils'); + +/** + * Fills the logs table with 200 logs and scrolls to the bottom to test autoscroll behavior. + * @param {Page} page - puppeteer page + */ +const fillTableAndScrollToBottom = async (page) => { + // ensure table has more rows than fit on screen and is scrolled to the bottom + // as the test case is where scrollTop clamps back to 0 + await injectLogs(page, Array.from({ length: 200 }, (_, i) => ({ + severity: 'I', + message: `info log ${i}`, + timestamp: Date.now() + i, + }))); + + await page.evaluate(() => model.log.goToLastItem()); + await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); +}; + +describe('Logs Table test-suite', async () => { + let page = null; + before(async () => { + ({ page } = test); + }); + + after(async () => { + await page.evaluate(() => model.log.liveStop('Query')); + }); + + it('should disable autoscroll when the user scrolls up in query mode', async () => { + await page.evaluate(() => model.log.liveStop('Query')); + await fillTableAndScrollToBottom(page); + + await page.evaluate(() => model.log.enableAutoScroll()); + let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + + const scrollTopAtBottom = await page.evaluate(() => window.model.log.scrollTop); + await page.evaluate(() => { + document.querySelector('.tableLogsContent').scrollTop -= 100; + }); + // model.log.scrollTop is only updated by the scroll handler, so this waits for it to have run + await page.waitForFunction( + (previous) => window.model.log.scrollTop < previous, + { timeout: 5000 }, + scrollTopAtBottom, + ); + + autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, false); + }); + + it('should disable autoscroll when the user scrolls up in live mode', async () => { + await page.click('#live-button'); + + let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + + // wait until live logs overflow the table and autoscroll has moved it down + await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); + + await page.evaluate(() => { + document.querySelector('.tableLogsContent').scrollTop = 100; + }); + await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); + + autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, false); + }); + + describe('should not disable autoscroll when table shrinks', async () => { + it('switching from a full query mode to live mode', async () => { + await page.evaluate(() => model.log.liveStop('Query')); + await fillTableAndScrollToBottom(page); + + await page.click('#live-button'); + await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); + + const autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + }); + + it('clearing log list in query mode', async () => { + await page.evaluate(() => model.log.liveStop('Query')); + + await page.evaluate(() => model.log.enableAutoScroll()); + let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + + await fillTableAndScrollToBottom(page); + + await page.click('#clear-button'); + await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); + + autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + }); + + it('clearing log list in live mode', async () => { + await page.click('#live-button'); + + let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + + // wait until logs are loaded and starting to scroll + await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); + + await page.click('#clear-button'); + // the scroll event of scrollTop fires on a later frame + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + + autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); + assert.strictEqual(autoScrollLive, true); + }); + }); +}); From 4e0f3f605dc2513cf9a289480aecb4b1ba440745 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:43:45 +0200 Subject: [PATCH 05/24] Fix commented out tests --- InfoLogger/test/mocha-index.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index 118eb97b5..2ee746510 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -107,14 +107,14 @@ describe('InfoLogger', function () { assert.deepStrictEqual(search, '?q={"severity":{"in":"I W E F"}}'); }); - // require('./public/user-actions-mocha'); - // require('./public/log-filter-actions-mocha'); - // require('./public/log-filter-url-mocha'); - // require('./public/live-mode-mocha'); - // require('./public/query-mode-mocha'); - // require('./public/status-bar-mocha'); - // require('./public/zoom.mocha'); - // require('./public/log-context-menu-mocha'); + require('./public/user-actions-mocha'); + require('./public/log-filter-actions-mocha'); + require('./public/log-filter-url-mocha'); + require('./public/live-mode-mocha'); + require('./public/query-mode-mocha'); + require('./public/status-bar-mocha'); + require('./public/zoom.mocha'); + require('./public/log-context-menu-mocha'); require('./public/logs-table-mocha'); after(async () => { From 70c9dc27fc341d42587f315101ff0c57a1610786 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:25:32 +0200 Subject: [PATCH 06/24] Stabilize logs table autoscroll tests Refactor the logs table mocha tests to wait for `autoScrollLive` state transitions with a shared helper instead of reading the value immediately. --- InfoLogger/test/public/logs-table-mocha.js | 52 +++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index dd2671021..56ade48b9 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -30,7 +30,16 @@ const fillTableAndScrollToBottom = async (page) => { }))); await page.evaluate(() => model.log.goToLastItem()); - await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); + await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); +}; + +/** + * Waits for autoScrollLive to be set to the expected value. + * @param {Page} page - puppeteer page + * @param {boolean} expected - expected value of autoScrollLive + */ +const waitForAutoScrollLiveToBe = async (page, expected) => { + await page.waitForFunction((expected) => model.log.autoScrollLive === expected, { timeout: 5000 }, expected); }; describe('Logs Table test-suite', async () => { @@ -48,40 +57,36 @@ describe('Logs Table test-suite', async () => { await fillTableAndScrollToBottom(page); await page.evaluate(() => model.log.enableAutoScroll()); - let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); + await waitForAutoScrollLiveToBe(page, true); - const scrollTopAtBottom = await page.evaluate(() => window.model.log.scrollTop); + const scrollTopAtBottom = await page.evaluate(() => model.log.scrollTop); await page.evaluate(() => { document.querySelector('.tableLogsContent').scrollTop -= 100; }); // model.log.scrollTop is only updated by the scroll handler, so this waits for it to have run await page.waitForFunction( - (previous) => window.model.log.scrollTop < previous, + (previous) => model.log.scrollTop < previous, { timeout: 5000 }, scrollTopAtBottom, ); - autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, false); + await waitForAutoScrollLiveToBe(page, false); }); it('should disable autoscroll when the user scrolls up in live mode', async () => { await page.click('#live-button'); - let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); + await waitForAutoScrollLiveToBe(page, true); // wait until live logs overflow the table and autoscroll has moved it down - await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); + await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); await page.evaluate(() => { document.querySelector('.tableLogsContent').scrollTop = 100; }); - await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); + await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); - autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, false); + await waitForAutoScrollLiveToBe(page, false); }); describe('should not disable autoscroll when table shrinks', async () => { @@ -90,43 +95,38 @@ describe('Logs Table test-suite', async () => { await fillTableAndScrollToBottom(page); await page.click('#live-button'); - await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); + await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); - const autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); + await waitForAutoScrollLiveToBe(page, true); }); it('clearing log list in query mode', async () => { await page.evaluate(() => model.log.liveStop('Query')); await page.evaluate(() => model.log.enableAutoScroll()); - let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); + await waitForAutoScrollLiveToBe(page, true); await fillTableAndScrollToBottom(page); await page.click('#clear-button'); - await page.waitForFunction(() => window.model.log.scrollTop === 0, { timeout: 5000 }); + await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); - autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); + await waitForAutoScrollLiveToBe(page, true); }); it('clearing log list in live mode', async () => { await page.click('#live-button'); - let autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); + await waitForAutoScrollLiveToBe(page, true); // wait until logs are loaded and starting to scroll - await page.waitForFunction(() => window.model.log.scrollTop > 0, { timeout: 5000 }); + await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); await page.click('#clear-button'); // the scroll event of scrollTop fires on a later frame await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); - autoScrollLive = await page.evaluate(() => window.model.log.autoScrollLive); - assert.strictEqual(autoScrollLive, true); + await waitForAutoScrollLiveToBe(page, true); }); }); }); From 5e9612091f3309c8f919d85ee2e9ddee5673a624 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:53:38 +0200 Subject: [PATCH 07/24] Fix failing test due to context menu overlay swallowing clicks --- InfoLogger/test/public/log-context-menu-mocha.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InfoLogger/test/public/log-context-menu-mocha.js b/InfoLogger/test/public/log-context-menu-mocha.js index ed2ba06e6..d194b37d3 100644 --- a/InfoLogger/test/public/log-context-menu-mocha.js +++ b/InfoLogger/test/public/log-context-menu-mocha.js @@ -91,6 +91,10 @@ describe('Cell Context Menu', async () => { }); }); + after(async () => { + await page.evaluate(() => model.log.contextMenu.hide()); + }); + describe('Menu visibility', async () => { it('should show context menu on right-click', async () => { await page.evaluate(() => { From 5c320132b05c12f08d7f0db689d0c3833e6bee33 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:54:01 +0200 Subject: [PATCH 08/24] Keep test setup consistent and testing through UI as much as possible --- InfoLogger/test/public/logs-table-mocha.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index 56ade48b9..3aa57943e 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -12,7 +12,6 @@ * or submit itself to any jurisdiction. */ -const assert = require('assert'); const test = require('../mocha-index'); const { injectLogs } = require('../utils/utils'); @@ -53,7 +52,7 @@ describe('Logs Table test-suite', async () => { }); it('should disable autoscroll when the user scrolls up in query mode', async () => { - await page.evaluate(() => model.log.liveStop('Query')); + await page.click('#query-button'); await fillTableAndScrollToBottom(page); await page.evaluate(() => model.log.enableAutoScroll()); From 1b0658a3c037cb7c95838be9cf039a035063be61 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:02:51 +0200 Subject: [PATCH 09/24] Testing what CI likes or not --- InfoLogger/test/public/logs-table-mocha.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index 3aa57943e..daff08060 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -12,6 +12,7 @@ * or submit itself to any jurisdiction. */ +const assert = require('assert'); const test = require('../mocha-index'); const { injectLogs } = require('../utils/utils'); @@ -56,7 +57,7 @@ describe('Logs Table test-suite', async () => { await fillTableAndScrollToBottom(page); await page.evaluate(() => model.log.enableAutoScroll()); - await waitForAutoScrollLiveToBe(page, true); + assert.strictEqual(await page.evaluate(() => model.log.autoScrollLive), true); const scrollTopAtBottom = await page.evaluate(() => model.log.scrollTop); await page.evaluate(() => { From 5a5ced5561cbe05043b67cc8b97dd8ecf13aa194 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:13:52 +0200 Subject: [PATCH 10/24] Refactor autoscroll assertions in logs table tests for clarity and efficiency --- InfoLogger/test/public/logs-table-mocha.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index daff08060..a2d54233d 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -38,8 +38,8 @@ const fillTableAndScrollToBottom = async (page) => { * @param {Page} page - puppeteer page * @param {boolean} expected - expected value of autoScrollLive */ -const waitForAutoScrollLiveToBe = async (page, expected) => { - await page.waitForFunction((expected) => model.log.autoScrollLive === expected, { timeout: 5000 }, expected); +const assertAutoScrollLive = async (page, expected) => { + assert.strictEqual(await page.evaluate(() => model.log.autoScrollLive), expected); }; describe('Logs Table test-suite', async () => { @@ -57,7 +57,7 @@ describe('Logs Table test-suite', async () => { await fillTableAndScrollToBottom(page); await page.evaluate(() => model.log.enableAutoScroll()); - assert.strictEqual(await page.evaluate(() => model.log.autoScrollLive), true); + await assertAutoScrollLive(page, true); const scrollTopAtBottom = await page.evaluate(() => model.log.scrollTop); await page.evaluate(() => { @@ -70,13 +70,13 @@ describe('Logs Table test-suite', async () => { scrollTopAtBottom, ); - await waitForAutoScrollLiveToBe(page, false); + await assertAutoScrollLive(page, false); }); it('should disable autoscroll when the user scrolls up in live mode', async () => { await page.click('#live-button'); - await waitForAutoScrollLiveToBe(page, true); + await assertAutoScrollLive(page, true); // wait until live logs overflow the table and autoscroll has moved it down await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); @@ -86,7 +86,7 @@ describe('Logs Table test-suite', async () => { }); await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); - await waitForAutoScrollLiveToBe(page, false); + await assertAutoScrollLive(page, false); }); describe('should not disable autoscroll when table shrinks', async () => { @@ -97,27 +97,27 @@ describe('Logs Table test-suite', async () => { await page.click('#live-button'); await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); - await waitForAutoScrollLiveToBe(page, true); + await assertAutoScrollLive(page, true); }); it('clearing log list in query mode', async () => { await page.evaluate(() => model.log.liveStop('Query')); await page.evaluate(() => model.log.enableAutoScroll()); - await waitForAutoScrollLiveToBe(page, true); + await assertAutoScrollLive(page, true); await fillTableAndScrollToBottom(page); await page.click('#clear-button'); await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); - await waitForAutoScrollLiveToBe(page, true); + await assertAutoScrollLive(page, true); }); it('clearing log list in live mode', async () => { await page.click('#live-button'); - await waitForAutoScrollLiveToBe(page, true); + await assertAutoScrollLive(page, true); // wait until logs are loaded and starting to scroll await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); @@ -126,7 +126,7 @@ describe('Logs Table test-suite', async () => { // the scroll event of scrollTop fires on a later frame await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); - await waitForAutoScrollLiveToBe(page, true); + await assertAutoScrollLive(page, true); }); }); }); From 11f05c5dd41f285f9934cd796fc1482fa72624a6 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:02:40 +0200 Subject: [PATCH 11/24] Make scroll check clearer --- InfoLogger/public/log/tableLogsContent.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index ee50f5b2f..2d87aab7c 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -211,13 +211,13 @@ const tableContainerHooks = (model) => ({ const { height } = container.getBoundingClientRect(); const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling - // LogsTable is emptied when switching to Live mode or clearing it. - // This causes a scroll event to be triggered as scrollTop is reset to 0. + // When LogsTable is emptied it causes a scroll event to be triggered as scrollTop is set to 0. // If scrollTop is smaller than before this is usually enough to tell if the user has scrolled up. - // But we also need to check if the user is not at the bottom to avoid treating the above cases as scrolls up. - // <=1 to catch fractional heights. - const isAtBottom = container.scrollHeight - container.clientHeight - scrollTop <= 1; - if (scrollTop < model.log.scrollTop && !isAtBottom) { + // But we need to check if the user remains at the bottom which is the case for the cases above. + // <=1 is used to catch fractional heights. + const atBottom = container.scrollHeight - container.clientHeight - scrollTop <= 1; + const scrollTopHasBecomeLess = scrollTop < model.log.scrollTop; + if (scrollTopHasBecomeLess && !atBottom) { model.log.disableAutoScroll(); } model.log.setScrollTop(scrollTop, height); From 04f6cbe8d87be1dd01d184298a0e7e01468e0dab Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:12:43 +0200 Subject: [PATCH 12/24] Fix JSDoc comment --- InfoLogger/test/public/logs-table-mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index a2d54233d..9217174f2 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -34,7 +34,7 @@ const fillTableAndScrollToBottom = async (page) => { }; /** - * Waits for autoScrollLive to be set to the expected value. + * Asserts that autoScrollLive is the expected value. * @param {Page} page - puppeteer page * @param {boolean} expected - expected value of autoScrollLive */ From 50c8c9b892bf5bab9d1bc9acdf4016dd803810b1 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Sun, 27 Sep 2026 22:59:11 +0200 Subject: [PATCH 13/24] Refactor autoscroll tests Refactor the table autoscroll tests into clearer query/live mode suites and added reusable helpers. --- InfoLogger/test/public/logs-table-mocha.js | 146 ++++++++++++--------- 1 file changed, 83 insertions(+), 63 deletions(-) diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index 9217174f2..8196bbb52 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -42,91 +42,111 @@ const assertAutoScrollLive = async (page, expected) => { assert.strictEqual(await page.evaluate(() => model.log.autoScrollLive), expected); }; +/** + * Returns the scroll position last recorded by the table's scroll handler. + * @param {Page} page - puppeteer page + * @returns {Promise} model.log.scrollTop + */ +const getScrollTop = (page) => page.evaluate(() => model.log.scrollTop); + +/** + * Waits until the table's scroll handler has recorded a scroll position below `previousScrollTop`. + * model.log.scrollTop is only updated by the scroll handler, so this proves it has run. + * @param {Page} page - puppeteer page + * @param {number} previousScrollTop - scroll position before the action under test + */ +const waitForScrollTopBelow = (page, previousScrollTop) => + page.waitForFunction((previous) => model.log.scrollTop < previous, { timeout: 5000 }, previousScrollTop); + describe('Logs Table test-suite', async () => { let page = null; + let baseUrl = null; + before(async () => { - ({ page } = test); + ({ helpers: { baseUrl }, page } = test); }); - after(async () => { - await page.evaluate(() => model.log.liveStop('Query')); + beforeEach(async () => { + await page.goto(baseUrl, { waitUntil: 'networkidle0' }); }); - it('should disable autoscroll when the user scrolls up in query mode', async () => { - await page.click('#query-button'); - await fillTableAndScrollToBottom(page); - - await page.evaluate(() => model.log.enableAutoScroll()); - await assertAutoScrollLive(page, true); - - const scrollTopAtBottom = await page.evaluate(() => model.log.scrollTop); - await page.evaluate(() => { - document.querySelector('.tableLogsContent').scrollTop -= 100; - }); - // model.log.scrollTop is only updated by the scroll handler, so this waits for it to have run - await page.waitForFunction( - (previous) => model.log.scrollTop < previous, - { timeout: 5000 }, - scrollTopAtBottom, - ); - - await assertAutoScrollLive(page, false); + after(async () => { + await page.evaluate(() => model.log.liveStop('Query')); }); - it('should disable autoscroll when the user scrolls up in live mode', async () => { - await page.click('#live-button'); - - await assertAutoScrollLive(page, true); - - // wait until live logs overflow the table and autoscroll has moved it down - await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); - - await page.evaluate(() => { - document.querySelector('.tableLogsContent').scrollTop = 100; + describe('Autoscroll behavior', async () => { + describe('in query mode', async () => { + it('should disable autoscroll when the user scrolls up', async () => { + await fillTableAndScrollToBottom(page); + await page.evaluate(() => model.log.enableAutoScroll()); + await assertAutoScrollLive(page, true); + + const scrollTopAtBottom = await getScrollTop(page); + await page.evaluate(() => { + document.querySelector('.tableLogsContent').scrollTop -= 100; + }); + await waitForScrollTopBelow(page, scrollTopAtBottom); + + await assertAutoScrollLive(page, false); + }); + + it('should not disable autoscroll when the log list is cleared', async () => { + await page.evaluate(() => model.log.enableAutoScroll()); + await fillTableAndScrollToBottom(page); + await assertAutoScrollLive(page, true); + + const scrollTopAtBottom = await getScrollTop(page); + await page.click('#clear-button'); + await waitForScrollTopBelow(page, scrollTopAtBottom); + + await assertAutoScrollLive(page, true); + }); }); - await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); - - await assertAutoScrollLive(page, false); - }); - describe('should not disable autoscroll when table shrinks', async () => { - it('switching from a full query mode to live mode', async () => { - await page.evaluate(() => model.log.liveStop('Query')); - await fillTableAndScrollToBottom(page); + describe('in live mode', async () => { + beforeEach(async () => { + await page.waitForSelector('#live-button:not([disabled])'); + }); - await page.click('#live-button'); - await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); + it('should disable autoscroll when the user scrolls up', async () => { + await page.click('#live-button'); + await assertAutoScrollLive(page, true); - await assertAutoScrollLive(page, true); - }); - - it('clearing log list in query mode', async () => { - await page.evaluate(() => model.log.liveStop('Query')); + // wait until live logs overflow the table and autoscroll has moved it down + await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); - await page.evaluate(() => model.log.enableAutoScroll()); - await assertAutoScrollLive(page, true); + const scrollTopAtBottom = await getScrollTop(page); + await page.evaluate(() => { + document.querySelector('.tableLogsContent').scrollTop -= 100; + }); + await waitForScrollTopBelow(page, scrollTopAtBottom); - await fillTableAndScrollToBottom(page); + await assertAutoScrollLive(page, false); + }); - await page.click('#clear-button'); - await page.waitForFunction(() => model.log.scrollTop === 0, { timeout: 5000 }); + it('should not disable autoscroll when switching from a full query table to live mode', async () => { + await fillTableAndScrollToBottom(page); - await assertAutoScrollLive(page, true); - }); + const scrollTopAtBottom = await getScrollTop(page); + await page.click('#live-button'); + await waitForScrollTopBelow(page, scrollTopAtBottom); - it('clearing log list in live mode', async () => { - await page.click('#live-button'); + await assertAutoScrollLive(page, true); + }); - await assertAutoScrollLive(page, true); + it('should not disable autoscroll when the log list is cleared', async () => { + await page.click('#live-button'); + await assertAutoScrollLive(page, true); - // wait until logs are loaded and starting to scroll - await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); + // wait until live logs overflow the table and autoscroll has moved it down + await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); - await page.click('#clear-button'); - // the scroll event of scrollTop fires on a later frame - await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + const scrollTopAtBottom = await getScrollTop(page); + await page.click('#clear-button'); + await waitForScrollTopBelow(page, scrollTopAtBottom); - await assertAutoScrollLive(page, true); + await assertAutoScrollLive(page, true); + }); }); }); }); From eb921bdbfe54bd677256bfe71ac67c826cbfb24b Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:24:11 +0200 Subject: [PATCH 14/24] Updating comments --- InfoLogger/public/log/tableLogsContent.js | 3 ++- InfoLogger/test/public/logs-table-mocha.js | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index 2d87aab7c..29ff64389 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -213,7 +213,8 @@ const tableContainerHooks = (model) => ({ // When LogsTable is emptied it causes a scroll event to be triggered as scrollTop is set to 0. // If scrollTop is smaller than before this is usually enough to tell if the user has scrolled up. - // But we need to check if the user remains at the bottom which is the case for the cases above. + // But we need to check if the user remains at the bottom, + // which is the case when switching to Live mode or clearing the table. // <=1 is used to catch fractional heights. const atBottom = container.scrollHeight - container.clientHeight - scrollTop <= 1; const scrollTopHasBecomeLess = scrollTop < model.log.scrollTop; diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index 8196bbb52..36ab74fd4 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -21,8 +21,7 @@ const { injectLogs } = require('../utils/utils'); * @param {Page} page - puppeteer page */ const fillTableAndScrollToBottom = async (page) => { - // ensure table has more rows than fit on screen and is scrolled to the bottom - // as the test case is where scrollTop clamps back to 0 + // ensure table has more rows than fit on the screen await injectLogs(page, Array.from({ length: 200 }, (_, i) => ({ severity: 'I', message: `info log ${i}`, @@ -43,17 +42,17 @@ const assertAutoScrollLive = async (page, expected) => { }; /** - * Returns the scroll position last recorded by the table's scroll handler. + * Returns the last saved scrollTop * @param {Page} page - puppeteer page * @returns {Promise} model.log.scrollTop */ const getScrollTop = (page) => page.evaluate(() => model.log.scrollTop); /** - * Waits until the table's scroll handler has recorded a scroll position below `previousScrollTop`. - * model.log.scrollTop is only updated by the scroll handler, so this proves it has run. + * Waits until the table's scroll handler has recorded a position below `previousScrollTop`. * @param {Page} page - puppeteer page * @param {number} previousScrollTop - scroll position before the action under test + * @returns {Promise} resolves when the scroll position is below `previousScrollTop` */ const waitForScrollTopBelow = (page, previousScrollTop) => page.waitForFunction((previous) => model.log.scrollTop < previous, { timeout: 5000 }, previousScrollTop); From 7cd80a438c4be9b2b14e75062033c226308c761c Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:18:12 +0200 Subject: [PATCH 15/24] Don't use onTableScroll for resizes No need to modify the autoScrollLive setting when a user resizes their browser so split the listeners up. --- InfoLogger/public/log/tableLogsContent.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index 29ff64389..43d66ecaf 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -203,12 +203,12 @@ const tableContainerHooks = (model) => ({ * @param {vnode} vnode - the vnode of the element */ oncreate(vnode) { + const container = vnode.dom; + /** - * THis handler allow to notify model of element scrolling change (.tableLogsContent) + * This handler allow to notify model of element scrolling change (.tableLogsContent) */ const onTableScroll = () => { - const container = vnode.dom; - const { height } = container.getBoundingClientRect(); const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling // When LogsTable is emptied it causes a scroll event to be triggered as scrollTop is set to 0. @@ -221,6 +221,15 @@ const tableContainerHooks = (model) => ({ if (scrollTopHasBecomeLess && !atBottom) { model.log.disableAutoScroll(); } + updateViewport(); + }; + + /** + * Update the viewport size and scroll position in the model + */ + const updateViewport = () => { + const { height } = container.getBoundingClientRect(); + const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling model.log.setScrollTop(scrollTop, height); }; @@ -228,10 +237,11 @@ const tableContainerHooks = (model) => ({ vnode.dom.addEventListener('scroll', onTableScroll); model.log.dom.table = vnode.dom; // setup window size listener - view needs redraw for smart scrolling - window.addEventListener('resize', onTableScroll); + window.addEventListener('resize', updateViewport); // remember this function for later (destroy) - vnode.dom.onTableScroll = onTableScroll; + container.onTableScroll = onTableScroll; + container.updateViewport = updateViewport; // call the function once on next frame when we know sizes onTableScroll(); @@ -251,7 +261,7 @@ const tableContainerHooks = (model) => ({ */ ondestroy(vnode) { vnode.dom.removeEventListener('scroll', vnode.dom.onTableScroll); - window.removeEventListener('resize', vnode.dom.onTableScroll); + window.removeEventListener('resize', vnode.dom.updateViewport); }, }); From 29acaeb2d2083c846784fef58931c170b581c62e Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:23:52 +0200 Subject: [PATCH 16/24] Refine log table scroll viewport updates --- InfoLogger/public/log/tableLogsContent.js | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index 43d66ecaf..fe2379415 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -206,12 +206,21 @@ const tableContainerHooks = (model) => ({ const container = vnode.dom; /** - * This handler allow to notify model of element scrolling change (.tableLogsContent) + * Update the viewport size and scroll position in the model + */ + const updateViewport = () => { + const { height } = container.getBoundingClientRect(); + const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling + model.log.setScrollTop(scrollTop, height); + }; + + /** + * Disable auto-scroll if the user scrolls up, then update the viewport. */ const onTableScroll = () => { const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling - // When LogsTable is emptied it causes a scroll event to be triggered as scrollTop is set to 0. + // When the logs table is emptied it causes a scroll event to be triggered as scrollTop is set to 0. // If scrollTop is smaller than before this is usually enough to tell if the user has scrolled up. // But we need to check if the user remains at the bottom, // which is the case when switching to Live mode or clearing the table. @@ -224,18 +233,9 @@ const tableContainerHooks = (model) => ({ updateViewport(); }; - /** - * Update the viewport size and scroll position in the model - */ - const updateViewport = () => { - const { height } = container.getBoundingClientRect(); - const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling - model.log.setScrollTop(scrollTop, height); - }; - // call the function when scrolling is updated - vnode.dom.addEventListener('scroll', onTableScroll); - model.log.dom.table = vnode.dom; + container.addEventListener('scroll', onTableScroll); + model.log.dom.table = container; // setup window size listener - view needs redraw for smart scrolling window.addEventListener('resize', updateViewport); @@ -244,7 +244,7 @@ const tableContainerHooks = (model) => ({ container.updateViewport = updateViewport; // call the function once on next frame when we know sizes - onTableScroll(); + updateViewport(); }, /** From aaad543ece5922f40ffa6851d2dd5a4c9744d5f1 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:31:40 +0200 Subject: [PATCH 17/24] Derive UI state from model Button style, live icon, browser tab title and auto-scroll were set inside the click handler via module-level state, so any mode change not triggered by a click left the UI stale. Derive them from log.activeMode in the model/view instead. --- InfoLogger/public/Model.js | 8 +- InfoLogger/public/common/utils.js | 2 +- InfoLogger/public/index.js | 1 - InfoLogger/public/log/Log.js | 45 ++++++--- InfoLogger/public/log/commandLogs.js | 105 ++++++--------------- InfoLogger/public/log/tableLogsContent.js | 2 +- InfoLogger/public/view.js | 59 +++++++++--- InfoLogger/test/public/query-mode-mocha.js | 25 +++-- InfoLogger/test/public/status-bar-mocha.js | 2 + 9 files changed, 129 insertions(+), 120 deletions(-) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index fbd4d5cb3..41e193f80 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -17,7 +17,7 @@ import { Observable, WebSocketClient, QueryRouter, Loader, RemoteData, sessionService, Notification, } from '/js/src/index.js'; -import { callRateLimiter, setBrowserTabTitle } from './common/utils.js'; +import { callRateLimiter } from './common/utils.js'; import { ConfigurationService } from './services/ConfigurationService.js'; import { MODE } from './constants/mode.const.js'; import Log from './log/Log.js'; @@ -115,12 +115,6 @@ export default class Model extends Observable { this.frameworkInfo = RemoteData.failure(result.message); } else { this.frameworkInfo = RemoteData.success(result); - if (result['infoLogger-gui'].name && result['infoLogger-gui'].name.trim()) { - window.ILG = { - name: `ILG - ${result['infoLogger-gui'].name}`, - }; - setBrowserTabTitle(window.ILG.name); - } } this.notify(); return; diff --git a/InfoLogger/public/common/utils.js b/InfoLogger/public/common/utils.js index cca66aa25..09f73523d 100644 --- a/InfoLogger/public/common/utils.js +++ b/InfoLogger/public/common/utils.js @@ -56,7 +56,7 @@ export function callRateLimiter(fn, time) { * @returns {void} */ export function setBrowserTabTitle(title = undefined) { - if (document && title) { + if (document && title && document.title !== title) { document.title = title; } } diff --git a/InfoLogger/public/index.js b/InfoLogger/public/index.js index 7b689a2b8..ef655c6ff 100644 --- a/InfoLogger/public/index.js +++ b/InfoLogger/public/index.js @@ -30,4 +30,3 @@ mount(document.body, view, model, debug); // Expose model to interact with it the browser's console window.model = model; -window.ILG = { name: 'ILG - ' }; diff --git a/InfoLogger/public/log/Log.js b/InfoLogger/public/log/Log.js index 91aae4be4..03608b52c 100644 --- a/InfoLogger/public/log/Log.js +++ b/InfoLogger/public/log/Log.js @@ -87,6 +87,22 @@ export default class Log extends Observable { return this.activeMode === MODE.QUERY; } + /** + * Toggles the live mode between running and paused + */ + toggleLiveMode() { + this.download.isVisible = false; + if (this.isLiveModeRunning()) { + this.liveStop(MODE.LIVE.PAUSED); + } else { + try { + this.liveStart(); + } catch (error) { + this.model.notification.show(error.toString(), 'danger', 3000); + } + } + } + /** * Toggle a dropdown with the full SQL query */ @@ -338,7 +354,7 @@ export default class Log extends Observable { * @returns {Promise} null if query is aborted, result of the query otherwise */ async query() { - if (!this.model.frameworkInfo.isSuccess() || !this.model.frameworkInfo.payload.mysql.status.ok) { + if (!this.isQueryModeAvailable()) { throw new Error('Query service is not available'); } @@ -354,6 +370,7 @@ export default class Log extends Observable { } else { this.activeMode = MODE.QUERY; } + this.download.isVisible = false; const previousQueryResult = this.queryResult; this.queryResult = RemoteData.loading(); @@ -463,10 +480,7 @@ export default class Log extends Observable { if (this.queryResult.isLoading()) { throw new Error('Query is loading, wait before starting live'); } - if (!this.model.ws.authed) { - throw new Error('WS is not yet ready'); - } - if (!this.model.frameworkInfo.isSuccess() || !this.model.frameworkInfo.payload.infoLoggerServer.status.ok) { + if (!this.isLiveModeAvailable()) { throw new Error('Live service is not available'); } if (this.isLiveModeRunning()) { @@ -486,6 +500,7 @@ export default class Log extends Observable { this.model.ws.setFilter(this.model.log.filter.toStringifyFunction()); + this.autoScrollLive = true; this.notify(); } @@ -500,6 +515,7 @@ export default class Log extends Observable { this.activeMode = mode; clearInterval(this.liveInterval); this.model.ws.setFilter(() => false); + this.autoScrollLive = false; this.notify(); } @@ -568,19 +584,22 @@ export default class Log extends Observable { } /** - * Enables auto-scroll, this is used when entering Live mode + * Returns whether the live mode service is available + * @returns {boolean} true if the live mode service is available, false otherwise */ - enableAutoScroll() { - this.autoScrollLive = true; - this.notify(); + isLiveModeAvailable() { + return Boolean(this.model.ws?.authed + && this.model.frameworkInfo.isSuccess() + && this.model.frameworkInfo.payload.infoLoggerServer?.status?.ok); } /** - * Disable auto-scroll, this is used when leaving Live mode + * Returns whether the query mode service is available + * @returns {boolean} true if the query mode service is available, false otherwise */ - disableAutoScroll() { - this.autoScrollLive = false; - this.notify(); + isQueryModeAvailable() { + return Boolean(this.model.frameworkInfo.isSuccess() + && this.model.frameworkInfo.payload.mysql?.status?.ok); } /** diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 06ca8932f..1b10e0d54 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -23,11 +23,16 @@ import { h, } from '/js/src/index.js'; import { BUTTON } from '../constants/button-states.const.js'; import { MODE } from '../constants/mode.const.js'; -import { setBrowserTabTitle } from '../common/utils.js'; -let queryButtonType = BUTTON.PRIMARY; -let liveButtonType = BUTTON.DEFAULT; -let liveButtonIcon = iconMediaPlay(); +/** + * Maps active modes to a live button style + * @type {Record vnode}>} + */ +const LIVE_BUTTON_TYPES_BY_MODE = { + [MODE.QUERY]: { className: BUTTON.DEFAULT, icon: iconMediaPlay }, + [MODE.LIVE.RUNNING]: { className: BUTTON.SUCCESS_ACTIVE, icon: iconMediaStop }, + [MODE.LIVE.PAUSED]: { className: BUTTON.PRIMARY, icon: iconMediaPlay }, +}; /** * Component for the command buttons (Query, Live, Clear, navigation between errors and download) @@ -81,10 +86,10 @@ const interactionModesGroupButton = (model) => { NotAsked: () => h('button.btn', { disabled: true }, ''), Loading: () => h('button.btn', { disabled: true, className: 'loading' }, 'Loading'), Failure: () => null, - Success: (frameworkInfo) => + Success: () => h('.btn-group', [ - queryButton(model, frameworkInfo), - liveButton(model, frameworkInfo), + queryButton(model.log), + liveButton(model.log), ]), }); }; @@ -94,14 +99,11 @@ const interactionModesGroupButton = (model) => { * - services lookup * - services result * - query lookup - * @param {Model} model - root model of the application - * @param {RemoteData.payload} frameworkInfo - the payload containing framework information + * @param {Log} logModel - log model of the application * @returns {vnode} - the view of the query button */ -const queryButton = (model, frameworkInfo) => { - const { log: logModel } = model; +const queryButton = (logModel) => { const { queryResult } = logModel; - const { mysql: { status: { ok: isDbReady = false } = {} } = {} } = frameworkInfo; if (queryResult.isLoading()) { return h('button.btn.bold', { @@ -112,12 +114,14 @@ const queryButton = (model, frameworkInfo) => { }, 'Cancel'); } + const isQueryModeAvailable = logModel.isQueryModeAvailable(); + return h('button.btn.bold', { id: 'query-button', - title: isDbReady ? 'Query database with filters (Enter)' : 'Query service not configured', - disabled: !isDbReady || queryResult.isLoading(), - className: queryButtonType, - onclick: () => toggleButtonStates(model, false), + title: isQueryModeAvailable ? 'Query database with filters (Enter)' : 'Query service not configured', + disabled: !isQueryModeAvailable, + className: logModel.isActiveModeQuery() ? BUTTON.PRIMARY : BUTTON.DEFAULT, + onclick: () => logModel.query(), }, 'Query'); }; @@ -126,25 +130,23 @@ const queryButton = (model, frameworkInfo) => { * - services lookup * - services result * - websocket status - * @param {Model} model - root model of the application - * @param {RemoteData.payload} frameworkInfo - the payload containing framework information + * @param {Log} logModel - log model of the application * @returns {vnode} - the view of the live button */ -const liveButton = (model, frameworkInfo) => { - const { log: logModel, ws } = model; - const { queryResult } = logModel; - const { authed: isWsAuthedAndReady = false } = ws; - const { infoLoggerServer: { status: { ok: isLiveServiceReady = false } = {} } = {} } = frameworkInfo; +const liveButton = (logModel) => { + const { queryResult, activeMode } = logModel; + const { className, icon } = LIVE_BUTTON_TYPES_BY_MODE[activeMode]; - const isLiveModeReady = isLiveServiceReady && isWsAuthedAndReady; - const title = isLiveModeReady ? 'Stream logs with filtering' : 'Live service not configured'; + const isLiveModeAvailable = logModel.isLiveModeAvailable(); + const title = isLiveModeAvailable ? 'Stream logs with filtering' : 'Live service not configured'; return h('button.btn.bold', { + id: 'live-button', title, - disabled: !isLiveModeReady || queryResult.isLoading(), - className: !isLiveModeReady ? 'loading' : liveButtonType, - onclick: () => toggleButtonStates(model, true), - }, 'Live', ' ', liveButtonIcon); + disabled: !isLiveModeAvailable || queryResult.isLoading(), + className: !isLiveModeAvailable ? 'loading' : className, + onclick: () => logModel.toggleLiveMode(), + }, 'Live', ' ', icon()); }; /** @@ -241,48 +243,3 @@ const zoomButtonGroup = (zoom) => title: 'Zoom in (Ctrl/Cmd + +)', }, h('span', { style: 'font-size:0.8em' }, iconPlus())), ]); - -/** - * Method to toggle states of the buttons(Query/Live) depending on the mode the tool is running on - * @param {Model} model - root model of the application - * @param {boolean} wasLivePressed - flag to check if the live button was pressed - */ -function toggleButtonStates(model, wasLivePressed) { - model.log.download.isVisible = false; // set visibility of download dropdown to false - if (wasLivePressed) { - switch (model.log.activeMode) { - case MODE.QUERY: - case MODE.LIVE.PAUSED: - try { - model.log.liveStart(); - setButtonsType(BUTTON.DEFAULT, BUTTON.SUCCESS_ACTIVE, iconMediaStop()); - model.log.enableAutoScroll(); - setBrowserTabTitle(`${window.ILG.name} LIVE`); - } catch (error) { - model.notification.show(error.toString(), 'danger', 3000); - } - break; - default: // MODE.LIVE.RUNNING - model.log.liveStop(MODE.LIVE.PAUSED); - setBrowserTabTitle(`${window.ILG.name} LIVE PAUSED`); - setButtonsType(BUTTON.DEFAULT, BUTTON.PRIMARY, iconMediaPlay()); - model.log.disableAutoScroll(); - } - } else { - model.log.query(); - setBrowserTabTitle(`${window.ILG.name} QUERY`); - setButtonsType(BUTTON.PRIMARY, BUTTON.DEFAULT, iconMediaPlay()); - } - - /** - * Method to change types of the buttons based on the mode being run - * @param {string} queryType Type of the Query Button - * @param {string} liveType Type of the Live Button - * @param {Icon} liveIcon Icon of the Live Button - */ - function setButtonsType(queryType, liveType, liveIcon) { - queryButtonType = queryType; - liveButtonType = liveType; - liveButtonIcon = liveIcon; - } -} diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index e0a207a48..2250078ca 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -211,7 +211,7 @@ const tableContainerHooks = (model) => ({ const { height } = container.getBoundingClientRect(); const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling if (container.scrollTop < model.log.scrollTop) { - model.log.disableAutoScroll(); // stop auto-scrolling if user scroll sup + model.log.autoScrollLive = false; // stop auto-scrolling if user scrolls up } model.log.setScrollTop(scrollTop, height); }; diff --git a/InfoLogger/public/view.js b/InfoLogger/public/view.js index 33cf83426..862f75c12 100644 --- a/InfoLogger/public/view.js +++ b/InfoLogger/public/view.js @@ -25,30 +25,59 @@ import tableLogsScrollMap from './log/tableLogsScrollMap.js'; import aboutComponent from './about/about.component.js'; import errorComponent from './common/errorComponent.js'; import { cellContextMenu } from './log/cellContextMenu.js'; +import { setBrowserTabTitle } from './common/utils.js'; +import { MODE } from './constants/mode.const.js'; + +const BROWSER_TAB_TITLE_BY_MODE = { + [MODE.QUERY]: 'QUERY', + [MODE.LIVE.RUNNING]: 'LIVE', + [MODE.LIVE.PAUSED]: 'LIVE PAUSED', +}; + +/** + * Given a frameworkInfo and the active mode, it will update the browser tab title accordingly + * @param {RemoteData} frameworkInfo - the framework information + * @param {string} activeMode - the current mode of the application + * @returns {void} + */ +function updateBrowserTabTitle(frameworkInfo, activeMode) { + const instanceName = frameworkInfo.isSuccess() + ? (frameworkInfo.payload['infoLogger-gui'].name || '').trim() + : ''; + const prefix = instanceName ? `ILG - ${instanceName}` : 'ILG'; + setBrowserTabTitle(`${prefix} ${BROWSER_TAB_TITLE_BY_MODE[activeMode]}`); +} /** * Main view of the application * @param {Model} model - root model of the application - * @returns {vnode} - the view of the application + * @returns {vnode[]} - the view of the application */ export default (model) => [ notification(model.notification), cellContextMenu(model), - h('.flex-column absolute-fill', [ - h('.shadow-level2', [ - h('header.p1.flex-row.f7.g1.justify-between', [ - h('.flex-row.g3', commandLogs(model)), - h('.flex-row.g3', commandFilters(model)), + h( + '.flex-column absolute-fill', + { + oncreate: () => updateBrowserTabTitle(model.frameworkInfo, model.log.activeMode), + onupdate: () => updateBrowserTabTitle(model.frameworkInfo, model.log.activeMode), + }, + [ + h('.shadow-level2', [ + h('header.p1.flex-row.f7.g1.justify-between', [ + h('.flex-row.g3', commandLogs(model)), + h('.flex-row.g3', commandFilters(model)), + ]), + h('header.f7', tableFilters(model)), + ]), + h('.flex-grow.flex-row.shadow-level0.logs-container', [ + aboutComponent(model), + logsTable(model), + inspectorSide(model), ]), - h('header.f7', tableFilters(model)), - ]), - h('.flex-grow.flex-row.shadow-level0.logs-container', [ - aboutComponent(model), - logsTable(model), - inspectorSide(model), - ]), - h('footer.f7.ph1', [statusBar(model)]), - ]), + h('footer.f7.ph1', [statusBar(model)]), + ], + ), ]; /** diff --git a/InfoLogger/test/public/query-mode-mocha.js b/InfoLogger/test/public/query-mode-mocha.js index f15ea3b11..0435a8193 100644 --- a/InfoLogger/test/public/query-mode-mocha.js +++ b/InfoLogger/test/public/query-mode-mocha.js @@ -51,8 +51,8 @@ const setupQueryTestState = (page) => window.confirm = () => true; window.model.frameworkInfo = { isSuccess: () => true, - payload: { mysql: { status: { ok: true } } }, - match: ({ Success }) => Success({ mysql: { status: { ok: true } } }), + payload: { mysql: { status: { ok: true } }, 'infoLogger-gui': { name: 'LOCAL-TEST' } }, + match: ({ Success }) => Success({ mysql: { status: { ok: true } }, 'infoLogger-gui': { name: 'LOCAL-TEST' } }), }; window.model.log.filter.resetCriteria(); window.model.log.empty(); @@ -95,14 +95,14 @@ const runQueryWithMocks = (page, { confirmReturn, textFilterOperator }) => postCalls += 1; return { ok: true, status: 200, - json: async () => [] }; + json: async () => ({ rows: [], count: 0 }) }; }; // Mock the frameworkInfo to make the query method think the query service is available in its check window.model.frameworkInfo = { isSuccess: () => true, - payload: { mysql: { status: { ok: true } } }, - match: ({ Success }) => Success({ mysql: { status: { ok: true } } }), + payload: { mysql: { status: { ok: true } }, 'infoLogger-gui': { name: 'LOCAL-TEST' } }, + match: ({ Success }) => Success({ mysql: { status: { ok: true } }, 'infoLogger-gui': { name: 'LOCAL-TEST' } }), }; // Default state of filters includes no text filters @@ -125,10 +125,9 @@ const runQueryWithMocks = (page, { confirmReturn, textFilterOperator }) => }); describe('Query Mode test-suite', async () => { - let page; - + let page = null; before(async () => { - page = test.page; + ({ page } = test); }); it('should fail because it is not configured', async () => { @@ -140,6 +139,16 @@ describe('Query Mode test-suite', async () => { } }); + it('should show correct browser tab title', async () => { + await page.evaluate(() => { + document.title = 'stale'; + }); + + await runQueryWithMocks(page, { confirmReturn: true }); + + await page.waitForFunction(() => document.title === 'ILG - LOCAL-TEST QUERY', { timeout: 1000 }); + }); + it('should copy multiple rows in the correct format', async () => { await injectLogs(page, [ { severity: 'I', message: 'info log', timestamp: Date.now() }, diff --git a/InfoLogger/test/public/status-bar-mocha.js b/InfoLogger/test/public/status-bar-mocha.js index 6002f4685..3c0d46609 100644 --- a/InfoLogger/test/public/status-bar-mocha.js +++ b/InfoLogger/test/public/status-bar-mocha.js @@ -79,6 +79,7 @@ describe('Status Bar test-suite', async () => { window.model.frameworkInfo = RemoteData.success({ mysql: { host: 'test-host.cern.ch', status: { ok: true } }, infoLoggerServer: { status: { ok: true } }, + 'infoLogger-gui': { name: 'LOCAL-TEST' }, }); window.model.log.activeMode = 'Running'; window.model.log.liveStartedAt = new Date(); @@ -94,6 +95,7 @@ describe('Status Bar test-suite', async () => { window.model.frameworkInfo = RemoteData.success({ mysql: { host: 'localhost', status: { ok: true } }, infoLoggerServer: { status: { ok: true } }, + 'infoLogger-gui': { name: 'LOCAL-TEST' }, }); window.model.log.activeMode = 'Query'; window.model.log.liveStartedAt = null; From 8847913109a7eaf0b6101ba38046c6350d987cc0 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:32:48 +0200 Subject: [PATCH 18/24] Improve autoscroll behavior and update status bar display in live mode --- InfoLogger/public/log/Log.js | 1 - InfoLogger/public/log/statusBar.js | 17 +++++-- InfoLogger/public/log/tableLogsContent.js | 11 ++-- InfoLogger/test/public/logs-table-mocha.js | 28 ----------- InfoLogger/test/public/status-bar-mocha.js | 58 +++++++++++++++++++--- 5 files changed, 69 insertions(+), 46 deletions(-) diff --git a/InfoLogger/public/log/Log.js b/InfoLogger/public/log/Log.js index 03608b52c..bf22cfef6 100644 --- a/InfoLogger/public/log/Log.js +++ b/InfoLogger/public/log/Log.js @@ -515,7 +515,6 @@ export default class Log extends Observable { this.activeMode = mode; clearInterval(this.liveInterval); this.model.ws.setFilter(() => false); - this.autoScrollLive = false; this.notify(); } diff --git a/InfoLogger/public/log/statusBar.js b/InfoLogger/public/log/statusBar.js index 79e08f66f..86bebdeb4 100644 --- a/InfoLogger/public/log/statusBar.js +++ b/InfoLogger/public/log/statusBar.js @@ -13,6 +13,7 @@ */ import { h } from '/js/src/index.js'; +import { MODE } from '../constants/mode.const.js'; /** * Bottom bar, showing status of the log's list and its details, @@ -96,11 +97,17 @@ const applicationMessage = (model) => model.log.list.length > model.log.applicat * @returns {vnode} - the view of the application options */ const applicationOptions = (model) => [ - h('label.d-inline', { title: 'Scroll down in live mode on new log incoming' }, h('input', { - type: 'checkbox', - checked: model.log.autoScrollLive, - onchange: () => model.log.toggleAutoScroll(), - }), ' Autoscroll'), + model.log.activeMode !== MODE.QUERY ? h( + 'label.d-inline', + { title: 'Scroll down in live mode on new log incoming' }, + h( + 'span', + { + className: model.log.autoScrollLive ? 'success' : 'warning', + }, + model.log.autoScrollLive ? 'Autoscroll Active' : 'Scroll to the bottom to resume autoscroll', + ), + ) : null, h('span.mh1'), h('label.d-inline', { title: 'Show details of selected log' }, h('input', { type: 'checkbox', diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index 8085b5100..24fe396a3 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -16,6 +16,7 @@ import { h } from '/js/src/index.js'; import { severityClass } from './severityUtils.js'; import tableColGroup from './tableColGroup.js'; +import { MODE } from '../constants/mode.const.js'; /** * Main content of ILG - simulates a big table scrolling. @@ -219,14 +220,14 @@ const tableContainerHooks = (model) => ({ */ const onTableScroll = () => { const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling + const { scrollHeight, clientHeight } = container; // When the logs table is emptied it causes a scroll event to be triggered as scrollTop is set to 0. // If scrollTop is smaller than before this is usually enough to tell if the user has scrolled up. - // But we need to check if the user remains at the bottom, - // which is the case when switching to Live mode or clearing the table. - // <=1 is used to catch fractional heights. - if (scrollTop < model.log.scrollTop) { - model.log.autoScrollLive = false; // stop auto-scrolling if user scrolls up + // However we have this situation described above where the table is emptied and scrollTop is set to 0. + if (model.log.activeMode !== MODE.QUERY) { + // <=1 tolerates fractional heights (zoom / HiDPI) + model.log.autoScrollLive = scrollHeight - scrollTop - clientHeight <= 1; } updateViewport(); }; diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index 36ab74fd4..321aaf485 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -74,34 +74,6 @@ describe('Logs Table test-suite', async () => { }); describe('Autoscroll behavior', async () => { - describe('in query mode', async () => { - it('should disable autoscroll when the user scrolls up', async () => { - await fillTableAndScrollToBottom(page); - await page.evaluate(() => model.log.enableAutoScroll()); - await assertAutoScrollLive(page, true); - - const scrollTopAtBottom = await getScrollTop(page); - await page.evaluate(() => { - document.querySelector('.tableLogsContent').scrollTop -= 100; - }); - await waitForScrollTopBelow(page, scrollTopAtBottom); - - await assertAutoScrollLive(page, false); - }); - - it('should not disable autoscroll when the log list is cleared', async () => { - await page.evaluate(() => model.log.enableAutoScroll()); - await fillTableAndScrollToBottom(page); - await assertAutoScrollLive(page, true); - - const scrollTopAtBottom = await getScrollTop(page); - await page.click('#clear-button'); - await waitForScrollTopBelow(page, scrollTopAtBottom); - - await assertAutoScrollLive(page, true); - }); - }); - describe('in live mode', async () => { beforeEach(async () => { await page.waitForSelector('#live-button:not([disabled])'); diff --git a/InfoLogger/test/public/status-bar-mocha.js b/InfoLogger/test/public/status-bar-mocha.js index 3c0d46609..924bfca99 100644 --- a/InfoLogger/test/public/status-bar-mocha.js +++ b/InfoLogger/test/public/status-bar-mocha.js @@ -35,7 +35,7 @@ async function getSeverityCounts(page) { } describe('Status Bar test-suite', async () => { - const AUTOSCROLL_SELECTOR = '#status-bar-application-options label[title*="Scroll down"] input'; + const AUTOSCROLL_SELECTOR = '#status-bar-application-options label[title*="Scroll down"] span'; const INSPECTOR_SELECTOR = '#status-bar-application-options label[title*="Show details"] input'; const SQL_QUERY = 'SELECT * FROM `messages` WHERE `timestamp`>=? AND `severity` IN (?) ' + 'ORDER BY `TIMESTAMP` LIMIT 100000'; @@ -319,14 +319,58 @@ describe('Status Bar test-suite', async () => { }); describe('Application Options', () => { - it('should toggle autoscroll when its checkbox is clicked', async () => { - await page.click(AUTOSCROLL_SELECTOR); + describe('Autoscroll Option', () => { + beforeEach(async () => { + await page.waitForSelector('#live-button:not([disabled])'); + }); - const checkedAfter = await page.$eval(AUTOSCROLL_SELECTOR, (el) => el.checked); - const autoScrollModelValue = await page.evaluate(() => window.model.log.autoScrollLive); + it('should not display autoscroll option when in query mode', async () => { + const autoscrollOption = await page.$(AUTOSCROLL_SELECTOR); + assert.strictEqual(autoscrollOption, null); + }); - assert.strictEqual(autoScrollModelValue, checkedAfter); - assert.strictEqual(checkedAfter, true); + it('should display autoscroll option when in live running mode', async () => { + await page.click('#live-button'); + + const autoscrollOption = await page.waitForSelector(AUTOSCROLL_SELECTOR); + assert.notStrictEqual(autoscrollOption, null); + }); + + it('should display autoscroll option when in live paused mode', async () => { + await page.click('#live-button'); + + const autoscrollOption = await page.waitForSelector(AUTOSCROLL_SELECTOR); + assert.notStrictEqual(autoscrollOption, null); + }); + + it('should show autoscroll as active when enabled in live mode', async () => { + await page.click('#live-button'); + await page.waitForSelector(AUTOSCROLL_SELECTOR); + + const message = 'Autoscroll Active'; + await page.waitForFunction( + (selector, text) => document.querySelector(selector)?.textContent.includes(text), + {}, + AUTOSCROLL_SELECTOR, + message, + ); + }); + + it('should show autoscroll as inactive when disabled in live mode', async () => { + await page.click('#live-button'); + await page.waitForSelector(AUTOSCROLL_SELECTOR); + await page.evaluate(() => { + model.log.autoScrollLive = false; + model.notify(); + }); + const message = 'Scroll to the bottom to resume autoscroll'; + await page.waitForFunction( + (selector, text) => document.querySelector(selector)?.textContent.includes(text), + {}, + AUTOSCROLL_SELECTOR, + message, + ); + }); }); it('should toggle inspector when its checkbox is clicked', async () => { From 0b54150caebc7644f2b4014f1696f4a869d6ec21 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:00:23 +0200 Subject: [PATCH 19/24] Notify on websocket authentication success --- InfoLogger/public/Model.js | 1 + 1 file changed, 1 insertion(+) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index 41e193f80..cda40dc3c 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -94,6 +94,7 @@ export default class Model extends Observable { handleWSAuthed() { // Tell server not to stream by default this.ws.setFilter(() => false); + this.notify(); } /** From ea3cc2b270bf5fcee26ea8c2ed8fc2afb100ceab Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:34:21 +0200 Subject: [PATCH 20/24] Improve live autoscroll reactivation UX The status bar now shows the autoscroll indicator only while live mode is running and the user cannot disable it. They can only enable it by scrolling to the bottom of the page or by clicking the label text. --- InfoLogger/public/log/Log.js | 7 +- InfoLogger/public/log/statusBar.js | 14 ++- InfoLogger/public/log/tableLogsContent.js | 127 +++++++++++++++++++--- 3 files changed, 124 insertions(+), 24 deletions(-) diff --git a/InfoLogger/public/log/Log.js b/InfoLogger/public/log/Log.js index 03608b52c..a98171394 100644 --- a/InfoLogger/public/log/Log.js +++ b/InfoLogger/public/log/Log.js @@ -575,11 +575,12 @@ export default class Log extends Observable { } /** - * Enable or disable auto-scroll for live mode, a checkbox is used to control it + * Enable auto-scroll and focus the log table */ - toggleAutoScroll() { - this.autoScrollLive = !this.autoScrollLive; + enableAutoScrollLive() { + this.autoScrollLive = true; this.dom.table.focus(); + this.dom.table.scrollTo(0, this.dom.table.scrollHeight); this.notify(); } diff --git a/InfoLogger/public/log/statusBar.js b/InfoLogger/public/log/statusBar.js index 86bebdeb4..423788132 100644 --- a/InfoLogger/public/log/statusBar.js +++ b/InfoLogger/public/log/statusBar.js @@ -97,15 +97,21 @@ const applicationMessage = (model) => model.log.list.length > model.log.applicat * @returns {vnode} - the view of the application options */ const applicationOptions = (model) => [ - model.log.activeMode !== MODE.QUERY ? h( + model.log.activeMode === MODE.LIVE.RUNNING ? h( 'label.d-inline', - { title: 'Scroll down in live mode on new log incoming' }, + { + title: model.log.autoScrollLive + ? 'Autoscroll is active' + : 'Click here / Scroll down to the bottom of the screen to reactivate', + }, h( 'span', { - className: model.log.autoScrollLive ? 'success' : 'warning', + id: 'status-bar-auto-scroll', + className: model.log.autoScrollLive ? 'success' : 'warning actionable-icon', + onclick: model.log.autoScrollLive ? undefined : () => model.log.enableAutoScrollLive(), }, - model.log.autoScrollLive ? 'Autoscroll Active' : 'Scroll to the bottom to resume autoscroll', + model.log.autoScrollLive ? 'Autoscroll Active' : '⚠️ Autoscroll Inactive', ), ) : null, h('span.mh1'), diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index 24fe396a3..bb2675a56 100644 --- a/InfoLogger/public/log/tableLogsContent.js +++ b/InfoLogger/public/log/tableLogsContent.js @@ -16,7 +16,6 @@ import { h } from '/js/src/index.js'; import { severityClass } from './severityUtils.js'; import tableColGroup from './tableColGroup.js'; -import { MODE } from '../constants/mode.const.js'; /** * Main content of ILG - simulates a big table scrolling. @@ -216,31 +215,115 @@ const tableContainerHooks = (model) => ({ }; /** - * Disable auto-scroll if the user scrolls up, then update the viewport. + * Whether the viewport is at the bottom of the table, <=1 tolerates fractional heights (zoom / HiDPI) + * @returns {boolean} true if at the bottom + */ + const isAtBottom = () => container.scrollHeight - Math.max(container.scrollTop, 0) - container.clientHeight <= 1; + + /** + * Record the direction of a user scroll; scrolling up disables auto-scroll straight away. + * Scroll events alone cannot tell a user scroll from a layout change (zoom, new logs, table emptied), + * so the user's intent is taken from their input instead. + * @param {boolean} isUp - true if the user scrolls towards older logs + */ + const onUserScroll = (isUp) => { + container.isUserScrollingDown = !isUp; + if (isUp && model.log.autoScrollLive && container.scrollTop > 0) { + model.log.autoScrollLive = false; + model.notify(); + } + }; + + /** + * Resume auto-scroll when the user scrolls to the bottom in live mode. + * Called from the input itself rather than waiting for the scroll to land, as a smooth scroll + * animation targets the bottom at the time of the input and falls short if logs arrive meanwhile. + */ + const resumeAutoScroll = () => { + if (model.log.isLiveModeRunning() && !model.log.autoScrollLive) { + model.log.autoScrollLive = true; + model.notify(); + } + }; + + /** + * Mouse wheel and touchpad scrolling + * @param {WheelEvent} e - wheel event + */ + const onWheel = (e) => { + // ctrl/cmd + wheel is zoom, handled by Model + if (e.ctrlKey || e.metaKey) { + return; + } + onUserScroll(e.deltaY < 0); + const maxScrollTop = container.scrollHeight - container.clientHeight; + if (e.deltaY > 0 && container.scrollTop + e.deltaY >= maxScrollTop - 1) { + resumeAutoScroll(); + } + }; + + /** + * Keyboard scrolling, arrow keys are handled by Model to move the selected log + * @param {KeyboardEvent} e - keyboard event + */ + const onKeyDown = (e) => { + if (e.target.tagName.toLowerCase() === 'input') { + return; + } + if (e.key === 'PageUp' || e.key === 'Home') { + onUserScroll(true); + } else if (e.key === 'End') { + onUserScroll(false); + resumeAutoScroll(); + } else if (e.key === 'PageDown') { + onUserScroll(false); + } + }; + + /** + * A pointer pressed on the container itself rather than a row is on the scrollbar + * @param {PointerEvent} e - pointer event + */ + const onPointerDown = (e) => { + container.isDraggingScrollbar = e.target === container; + }; + + /** + * End of a scrollbar drag + */ + const onPointerUp = () => { + container.isDraggingScrollbar = false; + }; + + /** + * Re-enable auto-scroll when the user reaches the bottom, then update the viewport. + * Disabling is done by the user input handlers above. */ const onTableScroll = () => { - const scrollTop = Math.max(container.scrollTop, 0); // cancel negative position due to Safari bounce scrolling - const { scrollHeight, clientHeight } = container; - - // When the logs table is emptied it causes a scroll event to be triggered as scrollTop is set to 0. - // If scrollTop is smaller than before this is usually enough to tell if the user has scrolled up. - // However we have this situation described above where the table is emptied and scrollTop is set to 0. - if (model.log.activeMode !== MODE.QUERY) { - // <=1 tolerates fractional heights (zoom / HiDPI) - model.log.autoScrollLive = scrollHeight - scrollTop - clientHeight <= 1; + // programmatic scroll is set when jumping to a selected log (e.g. the error navigation buttons) + if (container.isProgrammaticScroll) { + container.isProgrammaticScroll = false; + } else if (model.log.isLiveModeRunning()) { + if (container.isDraggingScrollbar) { + model.log.autoScrollLive = isAtBottom(); + } else if (container.isUserScrollingDown && isAtBottom()) { + model.log.autoScrollLive = true; + } } updateViewport(); }; - // call the function when scrolling is updated container.addEventListener('scroll', onTableScroll); + container.addEventListener('wheel', onWheel, { passive: true }); + container.addEventListener('pointerdown', onPointerDown); + window.addEventListener('pointerup', onPointerUp); + window.addEventListener('keydown', onKeyDown); model.log.dom.table = container; // setup window size listener - view needs redraw for smart scrolling window.addEventListener('resize', updateViewport); - // remember this function for later (destroy) - container.onTableScroll = onTableScroll; - container.updateViewport = updateViewport; + // remember these functions for later (destroy) + Object.assign(container, { onTableScroll, onWheel, onPointerDown, onPointerUp, onKeyDown, updateViewport }); // call the function once on next frame when we know sizes updateViewport(); @@ -259,8 +342,13 @@ const tableContainerHooks = (model) => ({ * @param {vnode} vnode - the vnode of the element */ ondestroy(vnode) { - vnode.dom.removeEventListener('scroll', vnode.dom.onTableScroll); - window.removeEventListener('resize', vnode.dom.updateViewport); + const container = vnode.dom; + container.removeEventListener('scroll', container.onTableScroll); + container.removeEventListener('wheel', container.onWheel); + container.removeEventListener('pointerdown', container.onPointerDown); + window.removeEventListener('pointerup', container.onPointerUp); + window.removeEventListener('keydown', container.onKeyDown); + window.removeEventListener('resize', container.updateViewport); }, }); @@ -298,7 +386,12 @@ const autoscrollManager = (model, vnode) => { const index = model.log.list.indexOf(model.log.item); const positionRow = model.log.rowHeight * index; const halfView = model.log.scrollHeight / 2; + const previousScrollTop = vnode.dom.scrollTop; vnode.dom.scrollTo(0, positionRow - halfView); + // scrollTo updates scrollTop synchronously; the event fires later and only if the position moved + if (vnode.dom.scrollTop !== previousScrollTop) { + vnode.dom.isProgrammaticScroll = true; + } } // Save the fact that we changed `item` From 1caead1e29ad1dd7ccda6ce1d75f06ca41ebf67d Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:40:29 +0200 Subject: [PATCH 21/24] Update autoscroll UI tests Adjust tests to match the current autoscroll behaviour in live mode. The logs table tests now simulate real wheel input and cover re-enabling autoscroll at the bottom and during zoom. --- InfoLogger/test/public/logs-table-mocha.js | 75 ++++++++++++++++------ InfoLogger/test/public/status-bar-mocha.js | 31 +++++++-- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/InfoLogger/test/public/logs-table-mocha.js b/InfoLogger/test/public/logs-table-mocha.js index 321aaf485..01e016fcc 100644 --- a/InfoLogger/test/public/logs-table-mocha.js +++ b/InfoLogger/test/public/logs-table-mocha.js @@ -42,20 +42,35 @@ const assertAutoScrollLive = async (page, expected) => { }; /** - * Returns the last saved scrollTop + * Scrolls the logs table with a real mouse wheel event, as autoscroll reacts to user input rather than scroll position * @param {Page} page - puppeteer page - * @returns {Promise} model.log.scrollTop + * @param {number} deltaY - wheel delta, negative scrolls up */ -const getScrollTop = (page) => page.evaluate(() => model.log.scrollTop); +const wheelOverTable = async (page, deltaY) => { + const box = await (await page.$('.tableLogsContent')).boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.wheel({ deltaY }); +}; /** - * Waits until the table's scroll handler has recorded a position below `previousScrollTop`. + * Allows waiting for a specified number of animation frames + * Useful when dealing with scroll and layout changes that require dealing with what occurs in each animation frame. * @param {Page} page - puppeteer page - * @param {number} previousScrollTop - scroll position before the action under test - * @returns {Promise} resolves when the scroll position is below `previousScrollTop` + * @param {*} frames - number of frames to wait for + * @returns {Promise} resolves after the specified number of animation frames */ -const waitForScrollTopBelow = (page, previousScrollTop) => - page.waitForFunction((previous) => model.log.scrollTop < previous, { timeout: 5000 }, previousScrollTop); +const waitForAnimationFrame = (page, frames = 1) => + page.evaluate((frames) => new Promise((resolve) => { + const step = () => { + if (frames <= 0) { + resolve(); + } else { + frames--; + requestAnimationFrame(step); + } + }; + step(); + }), frames); describe('Logs Table test-suite', async () => { let page = null; @@ -76,7 +91,7 @@ describe('Logs Table test-suite', async () => { describe('Autoscroll behavior', async () => { describe('in live mode', async () => { beforeEach(async () => { - await page.waitForSelector('#live-button:not([disabled])'); + await page.evaluate(() => model.log.liveStop('Query')); }); it('should disable autoscroll when the user scrolls up', async () => { @@ -86,21 +101,30 @@ describe('Logs Table test-suite', async () => { // wait until live logs overflow the table and autoscroll has moved it down await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); - const scrollTopAtBottom = await getScrollTop(page); - await page.evaluate(() => { - document.querySelector('.tableLogsContent').scrollTop -= 100; - }); - await waitForScrollTopBelow(page, scrollTopAtBottom); + await wheelOverTable(page, -100); + await waitForAnimationFrame(page, 2); + + await assertAutoScrollLive(page, false); + }); + + it('should re-enable autoscroll when the user scrolls back to the bottom', async () => { + await page.click('#live-button'); + await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); + await wheelOverTable(page, -100); + await waitForAnimationFrame(page, 2); await assertAutoScrollLive(page, false); + + await wheelOverTable(page, 100000); + await waitForAnimationFrame(page, 2); + await page.waitForFunction(() => model.log.autoScrollLive === true, { timeout: 5000 }); }); it('should not disable autoscroll when switching from a full query table to live mode', async () => { await fillTableAndScrollToBottom(page); - const scrollTopAtBottom = await getScrollTop(page); await page.click('#live-button'); - await waitForScrollTopBelow(page, scrollTopAtBottom); + await waitForAnimationFrame(page, 2); await assertAutoScrollLive(page, true); }); @@ -112,9 +136,24 @@ describe('Logs Table test-suite', async () => { // wait until live logs overflow the table and autoscroll has moved it down await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); - const scrollTopAtBottom = await getScrollTop(page); await page.click('#clear-button'); - await waitForScrollTopBelow(page, scrollTopAtBottom); + await waitForAnimationFrame(page, 2); + + await assertAutoScrollLive(page, true); + }); + + it('should not disable autoscroll when zooming in', async () => { + await page.click('#live-button'); + await assertAutoScrollLive(page, true); + await page.waitForFunction(() => model.log.scrollTop > 0, { timeout: 5000 }); + + await page.evaluate(() => new Promise((resolve) => { + // queue a scroll event as the autoscroll jump does (1px still counts as the bottom)... + model.log.dom.table.scrollTop -= 1; + // ...and zoom before that event is handled, as with a fast zoom click + model.zoom.zoomIn(); + requestAnimationFrame(() => requestAnimationFrame(resolve)); + })); await assertAutoScrollLive(page, true); }); diff --git a/InfoLogger/test/public/status-bar-mocha.js b/InfoLogger/test/public/status-bar-mocha.js index 924bfca99..6795f3b0f 100644 --- a/InfoLogger/test/public/status-bar-mocha.js +++ b/InfoLogger/test/public/status-bar-mocha.js @@ -35,7 +35,7 @@ async function getSeverityCounts(page) { } describe('Status Bar test-suite', async () => { - const AUTOSCROLL_SELECTOR = '#status-bar-application-options label[title*="Scroll down"] span'; + const AUTOSCROLL_SELECTOR = '#status-bar-auto-scroll'; const INSPECTOR_SELECTOR = '#status-bar-application-options label[title*="Show details"] input'; const SQL_QUERY = 'SELECT * FROM `messages` WHERE `timestamp`>=? AND `severity` IN (?) ' + 'ORDER BY `TIMESTAMP` LIMIT 100000'; @@ -47,6 +47,10 @@ describe('Status Bar test-suite', async () => { await page.goto(test.helpers.baseUrl, { waitUntil: 'networkidle0' }); }); + after(async () => { + await page.evaluate(() => model.log.liveStop('Query')); + }); + it('should be visible on the page', async () => { const statusBar = await page.$('#status-bar'); assert.ok(statusBar, 'status bar not found'); @@ -320,7 +324,7 @@ describe('Status Bar test-suite', async () => { describe('Application Options', () => { describe('Autoscroll Option', () => { - beforeEach(async () => { + before(async () => { await page.waitForSelector('#live-button:not([disabled])'); }); @@ -330,17 +334,21 @@ describe('Status Bar test-suite', async () => { }); it('should display autoscroll option when in live running mode', async () => { + // running mode await page.click('#live-button'); const autoscrollOption = await page.waitForSelector(AUTOSCROLL_SELECTOR); assert.notStrictEqual(autoscrollOption, null); }); - it('should display autoscroll option when in live paused mode', async () => { + it('should not display autoscroll option when in live paused mode', async () => { await page.click('#live-button'); - const autoscrollOption = await page.waitForSelector(AUTOSCROLL_SELECTOR); - assert.notStrictEqual(autoscrollOption, null); + await page.waitForFunction( + (selector) => !document.querySelector(selector), + { timeout: 2000 }, + AUTOSCROLL_SELECTOR, + ); }); it('should show autoscroll as active when enabled in live mode', async () => { @@ -357,13 +365,12 @@ describe('Status Bar test-suite', async () => { }); it('should show autoscroll as inactive when disabled in live mode', async () => { - await page.click('#live-button'); await page.waitForSelector(AUTOSCROLL_SELECTOR); await page.evaluate(() => { model.log.autoScrollLive = false; model.notify(); }); - const message = 'Scroll to the bottom to resume autoscroll'; + const message = '⚠️ Autoscroll Inactive'; await page.waitForFunction( (selector, text) => document.querySelector(selector)?.textContent.includes(text), {}, @@ -371,6 +378,16 @@ describe('Status Bar test-suite', async () => { message, ); }); + + it('should re-enable autoscroll when the inactive badge is clicked', async () => { + await page.click(AUTOSCROLL_SELECTOR); + await page.waitForFunction( + (selector) => document.querySelector(selector)?.textContent.includes('Autoscroll Active'), + {}, + AUTOSCROLL_SELECTOR, + ); + assert.strictEqual(await page.evaluate(() => model.log.autoScrollLive), true); + }); }); it('should toggle inspector when its checkbox is clicked', async () => { From 05a12bec347d4415037a8118728bfd18a9587e33 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:40:23 +0200 Subject: [PATCH 22/24] Update autoscroll status indicator The status bar now exposes the live autoscroll state with clearer active/inactive visuals. Inactive state is now a warning button that re-enables autoscroll when clicked. --- InfoLogger/public/log/statusBar.js | 24 ++++++++-------------- InfoLogger/test/public/status-bar-mocha.js | 4 ++-- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/InfoLogger/public/log/statusBar.js b/InfoLogger/public/log/statusBar.js index 423788132..0d98aea61 100644 --- a/InfoLogger/public/log/statusBar.js +++ b/InfoLogger/public/log/statusBar.js @@ -97,23 +97,15 @@ const applicationMessage = (model) => model.log.list.length > model.log.applicat * @returns {vnode} - the view of the application options */ const applicationOptions = (model) => [ - model.log.activeMode === MODE.LIVE.RUNNING ? h( - 'label.d-inline', - { - title: model.log.autoScrollLive - ? 'Autoscroll is active' - : 'Click here / Scroll down to the bottom of the screen to reactivate', - }, - h( - 'span', - { + model.log.activeMode === MODE.LIVE.RUNNING + ? model.log.autoScrollLive + ? h('span.success', { id: 'status-bar-auto-scroll', title: 'Autoscroll is active' }, 'Autoscroll Active') + : h('button.btn.btn-sm.btn-warning', { id: 'status-bar-auto-scroll', - className: model.log.autoScrollLive ? 'success' : 'warning actionable-icon', - onclick: model.log.autoScrollLive ? undefined : () => model.log.enableAutoScrollLive(), - }, - model.log.autoScrollLive ? 'Autoscroll Active' : '⚠️ Autoscroll Inactive', - ), - ) : null, + title: 'Click here / Scroll down to the bottom of the screen to reactivate', + onclick: () => model.log.enableAutoScrollLive(), + }, '⚠️ Autoscroll Inactive') + : null, h('span.mh1'), h('label.d-inline', { title: 'Show details of selected log' }, h('input', { type: 'checkbox', diff --git a/InfoLogger/test/public/status-bar-mocha.js b/InfoLogger/test/public/status-bar-mocha.js index 6795f3b0f..952ce6b88 100644 --- a/InfoLogger/test/public/status-bar-mocha.js +++ b/InfoLogger/test/public/status-bar-mocha.js @@ -36,7 +36,7 @@ async function getSeverityCounts(page) { describe('Status Bar test-suite', async () => { const AUTOSCROLL_SELECTOR = '#status-bar-auto-scroll'; - const INSPECTOR_SELECTOR = '#status-bar-application-options label[title*="Show details"] input'; + const INSPECTOR_SELECTOR = '#status-bar-appli cation-options label[title*="Show details"] input'; const SQL_QUERY = 'SELECT * FROM `messages` WHERE `timestamp`>=? AND `severity` IN (?) ' + 'ORDER BY `TIMESTAMP` LIMIT 100000'; @@ -379,7 +379,7 @@ describe('Status Bar test-suite', async () => { ); }); - it('should re-enable autoscroll when the inactive badge is clicked', async () => { + it('should re-enable autoscroll when inactive button is clicked', async () => { await page.click(AUTOSCROLL_SELECTOR); await page.waitForFunction( (selector) => document.querySelector(selector)?.textContent.includes('Autoscroll Active'), From f0820b83bf25a3303263a8466b29ba67c9d0ae75 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:46:44 +0200 Subject: [PATCH 23/24] Align status bar inspector checkbox layout Address checkbox box and label vertical centring issues. --- InfoLogger/public/app.css | 5 +++++ InfoLogger/public/log/statusBar.js | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/InfoLogger/public/app.css b/InfoLogger/public/app.css index b3decfe96..70d7ae993 100644 --- a/InfoLogger/public/app.css +++ b/InfoLogger/public/app.css @@ -264,3 +264,8 @@ a.disabled { pointer-events: none; cursor: default; } flex-direction: column; gap: 0.25rem; } + +.checkbox-container { + display: inline-flex; + gap: 6px; +} diff --git a/InfoLogger/public/log/statusBar.js b/InfoLogger/public/log/statusBar.js index 0d98aea61..1fdf1117e 100644 --- a/InfoLogger/public/log/statusBar.js +++ b/InfoLogger/public/log/statusBar.js @@ -26,8 +26,8 @@ export default (model) => [ h('', { style: 'width:50%' }, statusLogs(model)), h('', { style: 'text-align: center; width:30%' }, sqlQuery(model)), h( - '.flex-grow.text-right', - { id: 'status-bar-application-options' }, + '.flex-row.flex-grow.items-center', + { id: 'status-bar-application-options', style: 'justify-content: flex-end;' }, applicationMessage(model), applicationOptions(model), ), @@ -107,11 +107,11 @@ const applicationOptions = (model) => [ }, '⚠️ Autoscroll Inactive') : null, h('span.mh1'), - h('label.d-inline', { title: 'Show details of selected log' }, h('input', { + h('label.checkbox-container.m0.items-center', { title: 'Show details of selected log' }, h('input', { type: 'checkbox', checked: model.inspectorEnabled, onchange: () => model.toggleInspector(), - }), ' Inspector'), + }), 'Inspector'), ]; /** From 1d42fee4d32ffb1e35588cb1b6520be4908c688d Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:00:17 +0200 Subject: [PATCH 24/24] Fix test typo --- InfoLogger/test/public/status-bar-mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InfoLogger/test/public/status-bar-mocha.js b/InfoLogger/test/public/status-bar-mocha.js index 952ce6b88..144aecc5c 100644 --- a/InfoLogger/test/public/status-bar-mocha.js +++ b/InfoLogger/test/public/status-bar-mocha.js @@ -36,7 +36,7 @@ async function getSeverityCounts(page) { describe('Status Bar test-suite', async () => { const AUTOSCROLL_SELECTOR = '#status-bar-auto-scroll'; - const INSPECTOR_SELECTOR = '#status-bar-appli cation-options label[title*="Show details"] input'; + const INSPECTOR_SELECTOR = '#status-bar-application-options label[title*="Show details"] input'; const SQL_QUERY = 'SELECT * FROM `messages` WHERE `timestamp`>=? AND `severity` IN (?) ' + 'ORDER BY `TIMESTAMP` LIMIT 100000';