From 9c4525518ef0ff7668334287a7993ba5f8bc7b19 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 20 May 2026 23:19:56 +0200 Subject: [PATCH 1/9] [OGUI-1216] Override browser zoom to scale log table only Intercept Ctrl+scroll and Ctrl+/- to zoom the log table independently of the browser. Zoom state is tracked in the model, with font size and row height both being derived in rem from a multiplier. Added a reset button in the toolbar. --- InfoLogger/public/Model.js | 73 +++++++++++++++++++++++ InfoLogger/public/app.css | 10 +++- InfoLogger/public/log/Log.js | 9 ++- InfoLogger/public/log/commandLogs.js | 12 +++- InfoLogger/public/log/tableLogsContent.js | 9 ++- InfoLogger/public/view.js | 40 ++++++++++++- 6 files changed, 140 insertions(+), 13 deletions(-) diff --git a/InfoLogger/public/Model.js b/InfoLogger/public/Model.js index 308633a6a..cec707743 100644 --- a/InfoLogger/public/Model.js +++ b/InfoLogger/public/Model.js @@ -81,6 +81,17 @@ export default class Model extends Observable { // Model can change very often we protect router with callRateLimiter // Router limit: 100 calls per 30 seconds max = 30ms, 2 FPS is enough (500ms) this.observe(callRateLimiter(this.updateRouteOnModelChange.bind(this), 500)); + + this.zoom = { + level: 1, // Default zoom multiplier, 1 = 100% + min: 0.5, + max: 4, + step: 0.1, // Adds/subtracts 10% to the level + baseFontSize: 0.7, // Default font size in rem at level=1, matches default CSS .logs-container font size + // Row height in rem is fontSize * rowHeightRatio; keep this in sync with CSS variable --row-height + rowHeightRatio: 1.3, + lastScrollTime: 0, // Throttle zoom on scroll event to avoid too many updates, especially on a trackpad + }; } /** @@ -389,4 +400,66 @@ export default class Model extends Observable { isSecureContext() { return window.isSecureContext; } + + /** + * Font size in rem units + * @returns {number} - font size in rem units + */ + get fontSize() { + return this.zoom.baseFontSize * this.zoom.level; + } + + /** + * Row height in rem units, computed with font size to keep the same ratio across zoom levels + * @returns {number} - row height in rem units + */ + get rowHeightRem() { + return this.fontSize * this.zoom.rowHeightRatio; + } + + /** + * Row height in pixels, used for the virtual scroll to know how many logs to render depending on the container size + * @returns {number} - row height in pixels + */ + get rowHeightPx() { + return this.rowHeightRem * parseFloat(getComputedStyle(document.documentElement).fontSize); + } + + /** + * Zoom in by increasing zoom level by step, with a maximum of zoom.max + */ + zoomIn() { + this.#setZoomLevel(Math.min(this.zoom.level + this.zoom.step, this.zoom.max)); + } + + /** + * Zoom out by decreasing zoom level by step, with a minimum of zoom.min + */ + zoomOut() { + this.#setZoomLevel(Math.max(this.zoom.level - this.zoom.step, this.zoom.min)); + } + + /** + * Reset zoom to base level of 1 + */ + resetZoom() { + this.#setZoomLevel(1); + } + + /** + * Set zoom level + * @param {number} level - zoom level to set, should be between zoom.min and zoom.max + */ + #setZoomLevel(level) { + // Keep zoom to 2 d.p. to avoid floating-point artifacts (for example 1.2000000000000002) + // This keeps CSS values stable and ensures users can reliably return to default zoom (1) + this.zoom.level = parseFloat(level.toFixed(2)); + const root = document.querySelector('.logs-container'); + if (root) { + // Keep CSS sizes to 3 d.p. to avoid floating-point artifacts (for example 1.0920000000000002rem) + root.style.setProperty('--log-font-size', `${this.fontSize.toFixed(3)}rem`); + root.style.setProperty('--row-height', `${this.rowHeightRem.toFixed(3)}rem`); + } + this.notify(); + } } diff --git a/InfoLogger/public/app.css b/InfoLogger/public/app.css index f3d4cba4e..e5a1bfd45 100644 --- a/InfoLogger/public/app.css +++ b/InfoLogger/public/app.css @@ -23,7 +23,11 @@ .table-filters { width: 100%; } /* logs div */ -.logs-container { cursor: default; } +.logs-container { + cursor: default; + --log-font-size: 0.7rem; /* default, overridden by JS zoom */ + --row-height: 0.91rem; /* default, overridden by JS zoom */ +} .logs-content { border-top: 1px solid #aaa; } /* logs tables */ @@ -35,12 +39,12 @@ .table-logs-content td {} .table-logs-header td, -.table-logs-content td { font-size: 0.7rem; } +.table-logs-content td { font-size: var(--log-font-size); } td, th { max-width: 0; /* allow ellipsis on tables */ vertical-align: top; } -.cell { line-height: 1rem; font-size: 1rem; padding: 0rem 0.2rem; font-weight: 100; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 18px; /* must be sync with rowHeight constant in view */ } +.cell { line-height: var(--row-height); font-size: var(--log-font-size); padding: 0rem 0.2rem; font-weight: 100; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cell-bordered { border-left: 1px solid rgb(170, 170, 170); } .cell-xs { width: 2rem; } diff --git a/InfoLogger/public/log/Log.js b/InfoLogger/public/log/Log.js index a7f615841..12a5f3ff3 100644 --- a/InfoLogger/public/log/Log.js +++ b/InfoLogger/public/log/Log.js @@ -16,7 +16,6 @@ import { Observable, RemoteData } from '/js/src/index.js'; import LogFilter from '../logFilter/LogFilter.js'; import { MODE } from '../constants/mode.const.js'; import { TIME_MS } from '../common/Timezone.js'; -import { ROW_HEIGHT } from '../constants/visual.const.js'; /** * Model Log, encapsulate all log management and queries @@ -595,8 +594,12 @@ export default class Log extends Observable { */ listLogsInViewportOnly() { return this.list.slice( - Math.floor(this.scrollTop / ROW_HEIGHT), - Math.floor(this.scrollTop / ROW_HEIGHT) + Math.ceil(this.scrollHeight / ROW_HEIGHT) + 1, + Math.floor(this.scrollTop / this.rowHeight), + Math.floor(this.scrollTop / this.rowHeight) + Math.ceil(this.scrollHeight / this.rowHeight) + 1, ); } + + get rowHeight() { + return this.model.rowHeightPx; + } } diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 231ff2f6f..83b6694ac 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -12,7 +12,13 @@ * or submit itself to any jurisdiction. */ -import { h, iconPerson, iconMediaPlay, iconMediaStop, iconDataTransferDownload } from '/js/src/index.js'; +import { h, + iconPerson, + iconMediaPlay, + iconMediaStop, + iconDataTransferDownload, + iconMagnifyingGlass, +} 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'; @@ -58,6 +64,10 @@ export default (model) => [ title: 'Go to last log message (ALT + down arrow)', }, '↓'), downloadButtonGroup(model.log), + h('button.btn.flex-row', { + onclick: () => model.resetZoom(), + disabled: model.zoom.level === 1, + }, h('span', ['Reset ', iconMagnifyingGlass()])), ]; /** diff --git a/InfoLogger/public/log/tableLogsContent.js b/InfoLogger/public/log/tableLogsContent.js index 9c45c283b..190e54d88 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 { ROW_HEIGHT } from './../constants/visual.const.js'; /** * Main content of ILG - simulates a big table scrolling. @@ -34,7 +33,7 @@ export default (model) => tableContainerHooks(model), h('div.tableLogsContentPlaceholder', { style: { - height: `${model.log.list.length * ROW_HEIGHT}px`, + height: `${model.log.list.length * model.log.rowHeight}px`, position: 'relative', }, }, [ @@ -55,7 +54,7 @@ export default (model) => const scrollStyling = (model) => ({ style: { position: 'absolute', - top: `${model.log.scrollTop - model.log.scrollTop % ROW_HEIGHT}px`, + top: `${model.log.scrollTop - model.log.scrollTop % model.log.rowHeight}px`, }, }); @@ -182,7 +181,7 @@ const autoscrollManager = (model, vnode) => { if (previousLastLogId !== currentLastLogId) { // scroll at maximum bottom possible - vnode.dom.scrollTo(0, ROW_HEIGHT * model.log.applicationLimit); + vnode.dom.scrollTo(0, model.log.rowHeight * model.log.applicationLimit); vnode.dom.dataset.lastLogId = currentLastLogId; } @@ -199,7 +198,7 @@ const autoscrollManager = (model, vnode) => { if (previousSelectedItemId !== currentSelectedItemId && model.log.autoScrollToItem) { // scroll to an index * height of row, centered const index = model.log.list.indexOf(model.log.item); - const positionRow = ROW_HEIGHT * index; + const positionRow = model.log.rowHeight * index; const halfView = model.log.scrollHeight / 2; vnode.dom.scrollTo(0, positionRow - halfView); } diff --git a/InfoLogger/public/view.js b/InfoLogger/public/view.js index 8f744afce..36717f3eb 100644 --- a/InfoLogger/public/view.js +++ b/InfoLogger/public/view.js @@ -32,7 +32,45 @@ import errorComponent from './common/errorComponent.js'; */ export default (model) => [ notification(model.notification), - h('.flex-column absolute-fill', [ + h('.flex-column absolute-fill', { + oncreate: (vnode) => { + const handleWheel = (e) => { + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + const now = Date.now(); + if (now - model.zoom.lastScrollTime < 50) { + return; + } + model.zoom.lastScrollTime = now; + e.deltaY < 0 ? model.zoomIn() : model.zoomOut(); + }; + + const handleKeyDown = (e) => { + if (!e.ctrlKey) { + return; + } + // Support both '=' and '+' for zooming in, as some keyboards require Shift to type '+' + if (e.key === '=' || e.key === '+') { + e.preventDefault(); + model.zoomIn(); + } else if (e.key === '-') { + e.preventDefault(); + model.zoomOut(); + } + }; + + window.addEventListener('wheel', handleWheel, { passive: false }); + window.addEventListener('keydown', handleKeyDown); + + vnode.state.cleanup = () => { + window.removeEventListener('wheel', handleWheel); + window.removeEventListener('keydown', handleKeyDown); + }; + }, + onremove: (vnode) => vnode.state.cleanup(), + }, [ h('.shadow-level2', [ h('header.p1.flex-row.f7', [ h('', commandLogs(model)), From dcb30cfd214e5ac997fef23470f2b8f5714f3cd9 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 20 May 2026 23:37:42 +0200 Subject: [PATCH 2/9] [OGUI-1216] Add zoom feature test suite Tests cover: - Keyboard zoom - Mouse wheel zoom - Min/max clamping - Reset button - Preserve zoom ratio --- InfoLogger/test/mocha-index.js | 1 + InfoLogger/test/public/zoom.mocha.js | 215 +++++++++++++++++++++++++++ QualityControl/docker-compose.yml | 2 +- 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 InfoLogger/test/public/zoom.mocha.js diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index 051a18aa1..6e4d1b23f 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -104,6 +104,7 @@ describe('InfoLogger', function() { require('./public/log-filter-actions-mocha'); require('./public/live-mode-mocha'); require('./public/query-mode-mocha'); + require('./public/zoom.mocha'); after(async () => { await browser.close(); diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js new file mode 100644 index 000000000..f792ba094 --- /dev/null +++ b/InfoLogger/test/public/zoom.mocha.js @@ -0,0 +1,215 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. +*/ + +const assert = require('assert'); +const test = require('../mocha-index'); + +describe('Zoom test-suite', async () => { + let page; + + before(async () => { + page = test.page; + }); + + it('should have default zoom level of 1 with font size 0.7rem and matching row height', async () => { + const result = await page.evaluate(() => { + const container = document.querySelector('.logs-container'); + const style = getComputedStyle(container); + return { + fontSize: style.getPropertyValue('--log-font-size').trim(), + rowHeight: style.getPropertyValue('--row-height').trim(), + zoomLevel: window.model.zoom.level, + }; + }); + + assert.strictEqual(result.zoomLevel, 1, 'default zoom level should be 1'); + assert.strictEqual(result.fontSize, '0.7rem', 'default font size should be 0.7rem'); + assert.strictEqual(result.rowHeight, '0.91rem', 'default row height should be 0.91rem'); + }); + + it('should zoom in with Ctrl++', async () => { + await page.keyboard.down('Control'); + await page.keyboard.press('Equal'); + await page.keyboard.up('Control'); + + const result = await page.evaluate(() => { + const container = document.querySelector('.logs-container'); + const style = getComputedStyle(container); + return { + zoomLevel: window.model.zoom.level, + fontSize: style.getPropertyValue('--log-font-size').trim(), + }; + }); + + assert.strictEqual(result.zoomLevel, 1.1, 'zoom level should increase by 0.1'); + assert.strictEqual(result.fontSize, '0.770rem', 'font size should scale with zoom'); + }); + + it('should zoom out with Ctrl+-', async () => { + await page.keyboard.down('Control'); + await page.keyboard.press('Minus'); + await page.keyboard.up('Control'); + + const result = await page.evaluate(() => ({ + zoomLevel: window.model.zoom.level, + })); + + assert.strictEqual(result.zoomLevel, 1, 'zoom level should decrease back to 1'); + }); + + it('should not zoom below minimum level', async () => { + for (let i = 0; i < 10; i++) { + await page.keyboard.down('Control'); + await page.keyboard.press('Minus'); + await page.keyboard.up('Control'); + } + + const result = await page.evaluate(() => ({ + zoomLevel: window.model.zoom.level, + min: window.model.zoom.min, + })); + + assert.strictEqual(result.zoomLevel, result.min, 'zoom should not go below minimum'); + }); + + it('should not zoom above maximum level', async () => { + await page.evaluate(() => window.model.resetZoom()); + for (let i = 0; i < 35; i++) { + await page.keyboard.down('Control'); + await page.keyboard.press('Equal'); + await page.keyboard.up('Control'); + } + + const result = await page.evaluate(() => ({ + zoomLevel: window.model.zoom.level, + max: window.model.zoom.max, + })); + + assert.strictEqual(result.zoomLevel, result.max, 'zoom should not go above maximum'); + }); + + it('should reset zoom to default level', async () => { + await page.evaluate(() => window.model.resetZoom()); + + const result = await page.evaluate(() => { + const container = document.querySelector('.logs-container'); + const style = getComputedStyle(container); + return { + zoomLevel: window.model.zoom.level, + fontSize: style.getPropertyValue('--log-font-size').trim(), + rowHeight: style.getPropertyValue('--row-height').trim(), + }; + }); + + assert.strictEqual(result.zoomLevel, 1, 'zoom level should reset to 1'); + assert.strictEqual(result.fontSize, '0.700rem', 'font size should reset to default'); + assert.strictEqual(result.rowHeight, '0.910rem', 'row height should reset to default'); + }); + + it('should maintain row height ratio relative to font size across zoom levels', async () => { + const ratios = []; + for (let i = 0; i < 5; i++) { + const result = await page.evaluate(() => { + const fontSize = window.model.fontSize; + const rowHeight = window.model.rowHeightRem; + return { ratio: rowHeight / fontSize }; + }); + ratios.push(Number(result.ratio.toFixed(2))); + await page.evaluate(() => window.model.zoomIn()); + } + + const allSame = ratios.every((r) => r === ratios[0]); + assert.ok(allSame, `row height / font size ratio should be constant, got: ${ratios.join(', ')}`); + + await page.evaluate(() => window.model.resetZoom()); + }); + + it('should zoom in with mouse wheel (Ctrl+scroll up)', async () => { + const container = await page.$('.logs-container'); + const box = await container.boundingBox(); + const x = box.x + box.width / 2; + const y = box.y + box.height / 2; + + await page.mouse.move(x, y); + await page.evaluate(() => { + window.model.resetZoom(); + window.model.zoom.lastScrollTime = 0; + }); + + await page.evaluate((cx, cy) => { + const event = new WheelEvent('wheel', { + deltaY: -100, + ctrlKey: true, + bubbles: true, + clientX: cx, + clientY: cy, + }); + document.querySelector('.flex-column.absolute-fill').dispatchEvent(event); + }, x, y); + + const afterZoomIn = await page.evaluate(() => window.model.zoom.level); + assert.strictEqual(afterZoomIn, 1.1, 'Ctrl+scroll up should zoom in'); + }); + + it('should zoom out with mouse wheel (Ctrl+scroll down)', async () => { + await page.evaluate(() => { window.model.zoom.lastScrollTime = 0; }); + + const container = await page.$('.logs-container'); + const box = await container.boundingBox(); + const x = box.x + box.width / 2; + const y = box.y + box.height / 2; + + await page.evaluate((cx, cy) => { + const event = new WheelEvent('wheel', { + deltaY: 100, + ctrlKey: true, + bubbles: true, + clientX: cx, + clientY: cy, + }); + document.querySelector('.flex-column.absolute-fill').dispatchEvent(event); + }, x, y); + + const afterZoomOut = await page.evaluate(() => window.model.zoom.level); + assert.strictEqual(afterZoomOut, 1, 'Ctrl+scroll down should zoom out'); + }); + + it('should have reset button disabled at default zoom', async () => { + await page.evaluate(() => window.model.resetZoom()); + await new Promise((resolve) => setTimeout(resolve, 200)); + + const isDisabled = await page.evaluate(() => { + const buttons = [...document.querySelectorAll('button.btn')]; + const resetBtn = buttons.find((b) => b.textContent.includes('Reset')); + return resetBtn ? resetBtn.disabled : null; + }); + + assert.strictEqual(isDisabled, true, 'reset button should be disabled at default zoom'); + }); + + it('should have reset button enabled when zoomed', async () => { + await page.evaluate(() => window.model.zoomIn()); + await new Promise((resolve) => setTimeout(resolve, 200)); + + const isDisabled = await page.evaluate(() => { + const buttons = [...document.querySelectorAll('button.btn')]; + const resetBtn = buttons.find((b) => b.textContent.includes('Reset')); + return resetBtn ? resetBtn.disabled : null; + }); + + assert.strictEqual(isDisabled, false, 'reset button should be enabled when zoomed'); + + await page.evaluate(() => window.model.resetZoom()); + }); +}); diff --git a/QualityControl/docker-compose.yml b/QualityControl/docker-compose.yml index f191ad11d..5828d8206 100644 --- a/QualityControl/docker-compose.yml +++ b/QualityControl/docker-compose.yml @@ -6,7 +6,7 @@ services: environment: MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-cern}" ports: - - "3306:3306" + - "3307:3306" volumes: - type: volume source: database-data From 616d55c56d49934d7e79dddbcdf0af2ebf3fd133 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 21 May 2026 08:06:14 +0200 Subject: [PATCH 3/9] [OGUI-1216] Fix failing CI/CD tests --- InfoLogger/test/public/zoom.mocha.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js index f792ba094..dccd83c98 100644 --- a/InfoLogger/test/public/zoom.mocha.js +++ b/InfoLogger/test/public/zoom.mocha.js @@ -187,28 +187,20 @@ describe('Zoom test-suite', async () => { it('should have reset button disabled at default zoom', async () => { await page.evaluate(() => window.model.resetZoom()); - await new Promise((resolve) => setTimeout(resolve, 200)); - - const isDisabled = await page.evaluate(() => { + await page.waitForFunction(() => { const buttons = [...document.querySelectorAll('button.btn')]; const resetBtn = buttons.find((b) => b.textContent.includes('Reset')); - return resetBtn ? resetBtn.disabled : null; - }); - - assert.strictEqual(isDisabled, true, 'reset button should be disabled at default zoom'); + return resetBtn && resetBtn.disabled === true; + }, {timeout: 2000}); }); it('should have reset button enabled when zoomed', async () => { await page.evaluate(() => window.model.zoomIn()); - await new Promise((resolve) => setTimeout(resolve, 200)); - - const isDisabled = await page.evaluate(() => { + await page.waitForFunction(() => { const buttons = [...document.querySelectorAll('button.btn')]; const resetBtn = buttons.find((b) => b.textContent.includes('Reset')); - return resetBtn ? resetBtn.disabled : null; - }); - - assert.strictEqual(isDisabled, false, 'reset button should be enabled when zoomed'); + return resetBtn && resetBtn.disabled === false; + }, {timeout: 2000}); await page.evaluate(() => window.model.resetZoom()); }); From 99304c84fbbcc0f0e0cd8c4e40e000f5c319ae04 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 21 May 2026 08:11:16 +0200 Subject: [PATCH 4/9] [OGUI-1216] Try again to fix CI/CD failing test --- InfoLogger/public/log/commandLogs.js | 1 + InfoLogger/test/public/zoom.mocha.js | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 83b6694ac..13a6dc5a8 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -67,6 +67,7 @@ export default (model) => [ h('button.btn.flex-row', { onclick: () => model.resetZoom(), disabled: model.zoom.level === 1, + id: 'reset-zoom-button', }, h('span', ['Reset ', iconMagnifyingGlass()])), ]; diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js index dccd83c98..341bcd849 100644 --- a/InfoLogger/test/public/zoom.mocha.js +++ b/InfoLogger/test/public/zoom.mocha.js @@ -188,19 +188,17 @@ describe('Zoom test-suite', async () => { it('should have reset button disabled at default zoom', async () => { await page.evaluate(() => window.model.resetZoom()); await page.waitForFunction(() => { - const buttons = [...document.querySelectorAll('button.btn')]; - const resetBtn = buttons.find((b) => b.textContent.includes('Reset')); + const resetBtn = document.querySelector('#reset-zoom-button'); return resetBtn && resetBtn.disabled === true; - }, {timeout: 2000}); + }, { timeout: 2000 }); }); it('should have reset button enabled when zoomed', async () => { await page.evaluate(() => window.model.zoomIn()); await page.waitForFunction(() => { - const buttons = [...document.querySelectorAll('button.btn')]; - const resetBtn = buttons.find((b) => b.textContent.includes('Reset')); + const resetBtn = document.querySelector('#reset-zoom-button'); return resetBtn && resetBtn.disabled === false; - }, {timeout: 2000}); + }, { timeout: 2000 }); await page.evaluate(() => window.model.resetZoom()); }); From 16a465131cd54c247360f2fd936a867dc1824228 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 21 May 2026 08:34:38 +0200 Subject: [PATCH 5/9] [OGUI-1216] Increase tiemout for failing CI/CD test --- InfoLogger/test/public/zoom.mocha.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js index 341bcd849..693224b83 100644 --- a/InfoLogger/test/public/zoom.mocha.js +++ b/InfoLogger/test/public/zoom.mocha.js @@ -190,7 +190,7 @@ describe('Zoom test-suite', async () => { await page.waitForFunction(() => { const resetBtn = document.querySelector('#reset-zoom-button'); return resetBtn && resetBtn.disabled === true; - }, { timeout: 2000 }); + }); }); it('should have reset button enabled when zoomed', async () => { @@ -198,7 +198,7 @@ describe('Zoom test-suite', async () => { await page.waitForFunction(() => { const resetBtn = document.querySelector('#reset-zoom-button'); return resetBtn && resetBtn.disabled === false; - }, { timeout: 2000 }); + }); await page.evaluate(() => window.model.resetZoom()); }); From 569df463a3857fdeb489a14b4a814e9334c89a31 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 21 May 2026 09:05:40 +0200 Subject: [PATCH 6/9] [OGUI-1216] Calling Zoom In via keyboard --- InfoLogger/test/public/zoom.mocha.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js index 693224b83..7ae2a4a17 100644 --- a/InfoLogger/test/public/zoom.mocha.js +++ b/InfoLogger/test/public/zoom.mocha.js @@ -194,7 +194,10 @@ describe('Zoom test-suite', async () => { }); it('should have reset button enabled when zoomed', async () => { - await page.evaluate(() => window.model.zoomIn()); + await page.keyboard.down('Control'); + await page.keyboard.press('Equal'); + await page.keyboard.up('Control'); + await page.waitForFunction(() => { const resetBtn = document.querySelector('#reset-zoom-button'); return resetBtn && resetBtn.disabled === false; From 80eba669a70a8a0564b8f82a3f938790bb8cb87f Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 21 May 2026 09:19:23 +0200 Subject: [PATCH 7/9] [OGUI-1216] Revert to original, still unsure why failing --- InfoLogger/test/public/zoom.mocha.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js index 7ae2a4a17..2397cf261 100644 --- a/InfoLogger/test/public/zoom.mocha.js +++ b/InfoLogger/test/public/zoom.mocha.js @@ -194,9 +194,7 @@ describe('Zoom test-suite', async () => { }); it('should have reset button enabled when zoomed', async () => { - await page.keyboard.down('Control'); - await page.keyboard.press('Equal'); - await page.keyboard.up('Control'); + await page.evaluate(() => window.model.zoomIn()); await page.waitForFunction(() => { const resetBtn = document.querySelector('#reset-zoom-button'); From 259badf0c76524c04253989b1a1932ed64a4c387 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 21 May 2026 09:27:45 +0200 Subject: [PATCH 8/9] [OGUI-1216] Request the animation frame instead of polling --- InfoLogger/test/public/zoom.mocha.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js index 2397cf261..cade13ad0 100644 --- a/InfoLogger/test/public/zoom.mocha.js +++ b/InfoLogger/test/public/zoom.mocha.js @@ -187,19 +187,18 @@ describe('Zoom test-suite', async () => { it('should have reset button disabled at default zoom', async () => { await page.evaluate(() => window.model.resetZoom()); - await page.waitForFunction(() => { - const resetBtn = document.querySelector('#reset-zoom-button'); - return resetBtn && resetBtn.disabled === true; - }); + await page.evaluate(() => new Promise(requestAnimationFrame)); + + const disabled = await page.evaluate(() => document.querySelector('#reset-zoom-button')?.disabled); + assert.strictEqual(disabled, true, 'reset button should be disabled at default zoom'); }); it('should have reset button enabled when zoomed', async () => { await page.evaluate(() => window.model.zoomIn()); + await page.evaluate(() => new Promise(requestAnimationFrame)); - await page.waitForFunction(() => { - const resetBtn = document.querySelector('#reset-zoom-button'); - return resetBtn && resetBtn.disabled === false; - }); + const disabled = await page.evaluate(() => document.querySelector('#reset-zoom-button')?.disabled); + assert.strictEqual(disabled, false, 'reset button should be enabled when zoomed'); await page.evaluate(() => window.model.resetZoom()); }); From e4549d57828e8c6c20117da81ab51e5314f29db2 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 21 May 2026 09:37:16 +0200 Subject: [PATCH 9/9] [OGUI-1216] Revert again --- InfoLogger/test/public/zoom.mocha.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/InfoLogger/test/public/zoom.mocha.js b/InfoLogger/test/public/zoom.mocha.js index cade13ad0..693224b83 100644 --- a/InfoLogger/test/public/zoom.mocha.js +++ b/InfoLogger/test/public/zoom.mocha.js @@ -187,18 +187,18 @@ describe('Zoom test-suite', async () => { it('should have reset button disabled at default zoom', async () => { await page.evaluate(() => window.model.resetZoom()); - await page.evaluate(() => new Promise(requestAnimationFrame)); - - const disabled = await page.evaluate(() => document.querySelector('#reset-zoom-button')?.disabled); - assert.strictEqual(disabled, true, 'reset button should be disabled at default zoom'); + await page.waitForFunction(() => { + const resetBtn = document.querySelector('#reset-zoom-button'); + return resetBtn && resetBtn.disabled === true; + }); }); it('should have reset button enabled when zoomed', async () => { await page.evaluate(() => window.model.zoomIn()); - await page.evaluate(() => new Promise(requestAnimationFrame)); - - const disabled = await page.evaluate(() => document.querySelector('#reset-zoom-button')?.disabled); - assert.strictEqual(disabled, false, 'reset button should be enabled when zoomed'); + await page.waitForFunction(() => { + const resetBtn = document.querySelector('#reset-zoom-button'); + return resetBtn && resetBtn.disabled === false; + }); await page.evaluate(() => window.model.resetZoom()); });