From e2aef9a5a6dcf283fa2e44e9d356400a8de8c6f8 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 8 Jan 2026 12:10:39 +0100 Subject: [PATCH 01/12] feat: add new services endpoint for querying services information --- QualityControl/lib/QCModel.js | 5 ++++- QualityControl/lib/api.js | 1 + QualityControl/lib/controllers/StatusController.js | 10 ++++++++++ QualityControl/lib/services/BookkeepingService.js | 12 ++++++++++++ QualityControl/lib/services/Status.service.js | 13 +++++++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/QualityControl/lib/QCModel.js b/QualityControl/lib/QCModel.js index aa5364af6..127e512a5 100644 --- a/QualityControl/lib/QCModel.js +++ b/QualityControl/lib/QCModel.js @@ -102,7 +102,10 @@ export const setupQcModel = async (eventEmitter) => { const userController = new UserController(userRepository); const layoutController = new LayoutController(layoutRepository); - const statusService = new StatusService({ version: packageJSON?.version ?? '-' }, { qc: config.qc ?? {} }); + const statusService = new StatusService( + { version: packageJSON?.version ?? '-' }, + { qc: config.qc ?? {}, bookkeeping: config.bookkeeping ?? {} } + ); const statusController = new StatusController(statusService); const qcdbDownloadService = new QcdbDownloadService(config.ccdb); diff --git a/QualityControl/lib/api.js b/QualityControl/lib/api.js index 3c02671cd..a9c1df85f 100644 --- a/QualityControl/lib/api.js +++ b/QualityControl/lib/api.js @@ -102,6 +102,7 @@ export const setup = async (http, ws, eventEmitter) => { statusController.getServiceStatusHandler.bind(statusController), { public: true }, ); + http.get('/services', statusController.getServicesConfigurationHandler.bind(statusController)) http.get('/checkUser', userController.addUserHandler.bind(userController)); diff --git a/QualityControl/lib/controllers/StatusController.js b/QualityControl/lib/controllers/StatusController.js index 3e8ea025e..195cd6362 100644 --- a/QualityControl/lib/controllers/StatusController.js +++ b/QualityControl/lib/controllers/StatusController.js @@ -57,4 +57,14 @@ export class StatusController { ); } } + + /** + * Send back the configuration of the connected services for the frontend + * @param {Request} _ - HTTP request object + * @param {Response} res - HTTP response object + * @returns {undefined} + */ + async getServicesConfigurationHandler(_, res) { + res.status(200).json(this._statusService.retrieveServicesConfiguration()) + } } diff --git a/QualityControl/lib/services/BookkeepingService.js b/QualityControl/lib/services/BookkeepingService.js index f5d9f6353..8037aec89 100644 --- a/QualityControl/lib/services/BookkeepingService.js +++ b/QualityControl/lib/services/BookkeepingService.js @@ -181,6 +181,18 @@ export class BookkeepingService { } } + /** + * Retrieve the configured URL for Bookkeeping + * @returns {string | false} - URL for Bookkeeping, if not configured returns `false` + */ + retrieveBookkeepingURL() { + if (!this.active) { + this._logger.warnMessage('Bookkeeping not configured'); + return false; + } + return `${this._protocol}${this._hostname}${this._port}`; + } + /** * Helper method to construct a URL path with the required authentication token. * Appends the service's token as a query parameter to the provided path. diff --git a/QualityControl/lib/services/Status.service.js b/QualityControl/lib/services/Status.service.js index c6d6e33a9..57e45c258 100644 --- a/QualityControl/lib/services/Status.service.js +++ b/QualityControl/lib/services/Status.service.js @@ -120,6 +120,19 @@ export class StatusService { return { name: 'CCDB', status, version, extras: {} }; } + /** + * Retrieve the configurations of the services for the front end. + * @returns {object} - object containing the configurations of the services for the front end. + */ + retrieveServicesConfiguration() { + return { + bookkeeping: { + BASE_URL: this._config.bookkeeping.url, + PARTIAL_RUN_DETAILS: '?page=run-detail&run-number=', + }, + }; + } + /* * Getters & Setters */ From d389085735d50d3a59b80a6dd2e13f6d2a1f9ad0 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 8 Jan 2026 12:40:00 +0100 Subject: [PATCH 02/12] feat: add external link icon to the run number when bookkeeping is configured --- QualityControl/public/Model.js | 2 ++ QualityControl/public/app.css | 2 +- .../public/common/object/objectInfoCard.js | 17 ++++++++- .../public/services/Status.service.js | 35 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 QualityControl/public/services/Status.service.js diff --git a/QualityControl/public/Model.js b/QualityControl/public/Model.js index 203a6e58b..c46592c02 100644 --- a/QualityControl/public/Model.js +++ b/QualityControl/public/Model.js @@ -29,6 +29,7 @@ import AboutViewModel from './pages/aboutView/AboutViewModel.js'; import LayoutListModel from './pages/layoutListView/model/LayoutListModel.js'; import { RequestFields } from './common/RequestFields.enum.js'; import FilterModel from './common/filters/model/FilterModel.js'; +import StatusService from './services/Status.service.js'; /** * Represents the application's state and actions as a class @@ -97,6 +98,7 @@ export default class Model extends Observable { this.services = { object: new QCObjectService(this), layout: new LayoutService(this), + status: new StatusService(this), }; this.loader.get('/api/checkUser'); diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index 9cd733a09..f7c19da7f 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -147,7 +147,7 @@ font-weight: 500; } - &>div:hover { + & > div > div:hover { font-weight: 700; } } diff --git a/QualityControl/public/common/object/objectInfoCard.js b/QualityControl/public/common/object/objectInfoCard.js index 4474e29a9..1295351b2 100644 --- a/QualityControl/public/common/object/objectInfoCard.js +++ b/QualityControl/public/common/object/objectInfoCard.js @@ -13,6 +13,7 @@ */ import { h, isContextSecure } from '/js/src/index.js'; +import { iconExternalLink } from '/js/src/icons.js'; import { camelToTitleCase, copyToClipboard, prettyFormatDate } from './../utils.js'; const SPECIFIC_KEY_LABELS = { @@ -65,7 +66,21 @@ const infoRow = (key, value, infoRowAttributes) => { return h(`.flex-row.g2.info-row${highlightedClasses}`, [ h('b.w-25.w-wrapped', formattedKey), - h('.w-75.cursor-pointer', hasValue && infoRowAttributes(formattedKey, formattedValue), formattedValue), + h('.flex-row.w-75', [ + h( + '.cursor-pointer.flex-row', + hasValue && infoRowAttributes(formattedKey, formattedValue), + formattedValue, + ), + model.services.status.isConfigured('bookkeeping') && key === 'runNumber' + ? h('.ph2.text-right.actionable-icon.pointer-events-auto', { + title: 'Open run in Bookkeeping', + onclick: () => { + console.log(value); + }, + }, iconExternalLink()) + : '' + ]) ]); }; diff --git a/QualityControl/public/services/Status.service.js b/QualityControl/public/services/Status.service.js new file mode 100644 index 000000000..9f581cc43 --- /dev/null +++ b/QualityControl/public/services/Status.service.js @@ -0,0 +1,35 @@ +import { RemoteData } from '/js/src/index.js' + +export default class StatusService { + /** + * Initialize service + * @param {Model} model - root model of the application + */ + constructor(model) { + this.model = model; + + this.loader = model.loader; + this.serviceConfig = RemoteData.notAsked(); + + this.initStatusService(); + } + + async initStatusService() { + const { result, ok } = await this.loader.get('api/services') + if (ok) { + this.serviceConfig = RemoteData.success(result || {}); + } else { + this.serviceConfig = RemoteData.failure('Error retrieving services'); + } + + this.model.notify(); + } + + isConfigured(service) { + if (!this.serviceConfig.isSuccess()) { + return false; + } + + return this.serviceConfig.payload.hasOwnProperty(service); + } +} From ba7882d329d2d47237596e2708615c96f8755512 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 8 Jan 2026 12:48:26 +0100 Subject: [PATCH 03/12] fix: update partial run details to proper link structure --- QualityControl/lib/services/Status.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/lib/services/Status.service.js b/QualityControl/lib/services/Status.service.js index 57e45c258..515020806 100644 --- a/QualityControl/lib/services/Status.service.js +++ b/QualityControl/lib/services/Status.service.js @@ -128,7 +128,7 @@ export class StatusService { return { bookkeeping: { BASE_URL: this._config.bookkeeping.url, - PARTIAL_RUN_DETAILS: '?page=run-detail&run-number=', + PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=', }, }; } From ab22f44461d60414f6d31cffa68f9ed44311f07b Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 8 Jan 2026 12:58:28 +0100 Subject: [PATCH 04/12] fix: bookkeeping service still being present in service config --- QualityControl/lib/QCModel.js | 2 ++ QualityControl/lib/services/Status.service.js | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/QualityControl/lib/QCModel.js b/QualityControl/lib/QCModel.js index 127e512a5..3a872015f 100644 --- a/QualityControl/lib/QCModel.js +++ b/QualityControl/lib/QCModel.js @@ -119,6 +119,8 @@ export const setupQcModel = async (eventEmitter) => { const intervalsService = new IntervalsService(); const bookkeepingService = new BookkeepingService(config.bookkeeping); + statusService.bookkeepingService = bookkeepingService; + const filterService = new FilterService(bookkeepingService, config); const runModeService = new RunModeService(config.bookkeeping, bookkeepingService, ccdbService, eventEmitter); const objectController = new ObjectController(qcObjectService, runModeService, qcdbDownloadService); diff --git a/QualityControl/lib/services/Status.service.js b/QualityControl/lib/services/Status.service.js index 515020806..c9a1e1f89 100644 --- a/QualityControl/lib/services/Status.service.js +++ b/QualityControl/lib/services/Status.service.js @@ -38,6 +38,11 @@ export class StatusService { */ this._dataService = undefined; + /** + * @type {BookkeepingService} + */ + this._bookkeepingService = undefined; + /** * @type {WebSocket} */ @@ -125,12 +130,16 @@ export class StatusService { * @returns {object} - object containing the configurations of the services for the front end. */ retrieveServicesConfiguration() { - return { - bookkeeping: { + const serviceConfig = {}; + + if (this._bookkeepingService?.active) { + serviceConfig.bookkeeping = { BASE_URL: this._config.bookkeeping.url, PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=', - }, - }; + }; + } + + return serviceConfig; } /* @@ -146,6 +155,15 @@ export class StatusService { this._dataService = dataService; } + /** + * Set service to be used for querying status of the Bookkeeping service. + * @param {BookkeepingService} bookkeepingService - service used for retrieving Bookkeeping status + * @returns {void} + */ + set bookkeepingService(bookkeepingService) { + this._bookkeepingService = bookkeepingService; + } + /** * Set instance of websocket server * @param {WebSocket} ws - instance of the WS server From b018bb6616b30bd1ac7bac42ca02fc7a27650ff3 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 8 Jan 2026 13:12:08 +0100 Subject: [PATCH 05/12] feat: add external link button for opening runs in bookkeeping if bookkeeping is configured --- .../public/common/object/objectInfoCard.js | 9 ++-- .../public/services/Status.service.js | 49 ++++++++++++++++++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/QualityControl/public/common/object/objectInfoCard.js b/QualityControl/public/common/object/objectInfoCard.js index 1295351b2..187610f18 100644 --- a/QualityControl/public/common/object/objectInfoCard.js +++ b/QualityControl/public/common/object/objectInfoCard.js @@ -73,13 +73,12 @@ const infoRow = (key, value, infoRowAttributes) => { formattedValue, ), model.services.status.isConfigured('bookkeeping') && key === 'runNumber' - ? h('.ph2.text-right.actionable-icon.pointer-events-auto', { + ? h('a.ph2.text-right.actionable-icon.pointer-events-auto', { title: 'Open run in Bookkeeping', - onclick: () => { - console.log(value); - }, + href: model.services.status.buildBookkeepingUrl(value), + target: '_blank', }, iconExternalLink()) - : '' + : '', ]) ]); }; diff --git a/QualityControl/public/services/Status.service.js b/QualityControl/public/services/Status.service.js index 9f581cc43..e03a6e02d 100644 --- a/QualityControl/public/services/Status.service.js +++ b/QualityControl/public/services/Status.service.js @@ -1,5 +1,26 @@ +/** + * @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. + */ + import { RemoteData } from '/js/src/index.js' +/** + * @typedef {object} ServicePayload + * @property {object} [bookkeeping] - Configuration for the Bookkeeping service. + * @property {string} bookkeeping.BASE_URL - The root URL of the Bookkeeping application. + * @property {string} bookkeeping.PARTIAL_RUN_DETAILS - The URL path/query parameters for run details. + */ + export default class StatusService { /** * Initialize service @@ -7,13 +28,21 @@ export default class StatusService { */ constructor(model) { this.model = model; - this.loader = model.loader; + + /** + * @type {RemoteData} + */ this.serviceConfig = RemoteData.notAsked(); this.initStatusService(); } + /** + * Fetches service configurations from the backend and updates the internal state. + * Notifies the model once the request completes (success or failure). + * @returns {Promise} + */ async initStatusService() { const { result, ok } = await this.loader.get('api/services') if (ok) { @@ -25,6 +54,11 @@ export default class StatusService { this.model.notify(); } + /** + * Checks if a specific service configuration is successfully loaded and available. + * @param {string} service - The name of the service to check (e.g. 'bookkeeping'). + * @returns {boolean} - True if the service key exists in a successful payload. + */ isConfigured(service) { if (!this.serviceConfig.isSuccess()) { return false; @@ -32,4 +66,17 @@ export default class StatusService { return this.serviceConfig.payload.hasOwnProperty(service); } + + /** + * Constructs a full URL for the bookkeeping run details page. + * @param {string|number} runNumber - The specific run identifier to append to the URL. + * @returns {string|undefined} The formatted URL, or `undefined` if the service is not configured. + */ + buildBookkeepingUrl(runNumber) { + if (!this.isConfigured('bookkeeping')) { + return; + } + const { BASE_URL, PARTIAL_RUN_DETAILS } = this.serviceConfig.payload.bookkeeping; + return `${BASE_URL}/${PARTIAL_RUN_DETAILS}${runNumber}`; + } } From 814d500623b851bac83ac2b9dfe09edd19103330 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 8 Jan 2026 13:33:19 +0100 Subject: [PATCH 06/12] style: fix linting errors --- QualityControl/lib/QCModel.js | 2 +- QualityControl/lib/api.js | 2 +- QualityControl/lib/controllers/StatusController.js | 2 +- .../public/common/object/objectInfoCard.js | 2 +- QualityControl/public/services/Status.service.js | 13 ++++++------- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/QualityControl/lib/QCModel.js b/QualityControl/lib/QCModel.js index 3a872015f..afad949cd 100644 --- a/QualityControl/lib/QCModel.js +++ b/QualityControl/lib/QCModel.js @@ -104,7 +104,7 @@ export const setupQcModel = async (eventEmitter) => { const statusService = new StatusService( { version: packageJSON?.version ?? '-' }, - { qc: config.qc ?? {}, bookkeeping: config.bookkeeping ?? {} } + { qc: config.qc ?? {}, bookkeeping: config.bookkeeping ?? {} }, ); const statusController = new StatusController(statusService); diff --git a/QualityControl/lib/api.js b/QualityControl/lib/api.js index a9c1df85f..b63b0c999 100644 --- a/QualityControl/lib/api.js +++ b/QualityControl/lib/api.js @@ -102,7 +102,7 @@ export const setup = async (http, ws, eventEmitter) => { statusController.getServiceStatusHandler.bind(statusController), { public: true }, ); - http.get('/services', statusController.getServicesConfigurationHandler.bind(statusController)) + http.get('/services', statusController.getServicesConfigurationHandler.bind(statusController)); http.get('/checkUser', userController.addUserHandler.bind(userController)); diff --git a/QualityControl/lib/controllers/StatusController.js b/QualityControl/lib/controllers/StatusController.js index 195cd6362..0f6de3a63 100644 --- a/QualityControl/lib/controllers/StatusController.js +++ b/QualityControl/lib/controllers/StatusController.js @@ -65,6 +65,6 @@ export class StatusController { * @returns {undefined} */ async getServicesConfigurationHandler(_, res) { - res.status(200).json(this._statusService.retrieveServicesConfiguration()) + res.status(200).json(this._statusService.retrieveServicesConfiguration()); } } diff --git a/QualityControl/public/common/object/objectInfoCard.js b/QualityControl/public/common/object/objectInfoCard.js index 187610f18..9a025ce6b 100644 --- a/QualityControl/public/common/object/objectInfoCard.js +++ b/QualityControl/public/common/object/objectInfoCard.js @@ -79,7 +79,7 @@ const infoRow = (key, value, infoRowAttributes) => { target: '_blank', }, iconExternalLink()) : '', - ]) + ]), ]); }; diff --git a/QualityControl/public/services/Status.service.js b/QualityControl/public/services/Status.service.js index e03a6e02d..a64c9bcd0 100644 --- a/QualityControl/public/services/Status.service.js +++ b/QualityControl/public/services/Status.service.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { RemoteData } from '/js/src/index.js' +import { RemoteData } from '/js/src/index.js'; /** * @typedef {object} ServicePayload @@ -44,7 +44,7 @@ export default class StatusService { * @returns {Promise} */ async initStatusService() { - const { result, ok } = await this.loader.get('api/services') + const { result, ok } = await this.loader.get('api/services'); if (ok) { this.serviceConfig = RemoteData.success(result || {}); } else { @@ -60,11 +60,10 @@ export default class StatusService { * @returns {boolean} - True if the service key exists in a successful payload. */ isConfigured(service) { - if (!this.serviceConfig.isSuccess()) { - return false; - } - - return this.serviceConfig.payload.hasOwnProperty(service); + return this.serviceConfig.match({ + Success: (config) => Object.hasOwn(config, service), + Other: () => false, + }); } /** From 205d7ce029a93c48e9b505f49714c7be2619ddba Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 8 Jan 2026 13:44:16 +0100 Subject: [PATCH 07/12] test: fix failing test due to markup changes --- QualityControl/test/public/pages/object-tree.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 31284c3e5..a4b3a14bc 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -189,7 +189,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) const context = page.browserContext(); await context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']); - await page.click('#qcObjectInfoPanel > div > div'); + await page.click('#qcObjectInfoPanel > div > div > div'); const clipboard = await page.evaluate(async () => { await new Promise((resolve) => setTimeout(resolve, 500)); @@ -208,7 +208,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) const context = page.browserContext(); await context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']); - await page.click('#qcObjectInfoPanel > div > div'); // copy path + await page.click('#qcObjectInfoPanel > div > div > div'); // copy path await page.click('#qcObjectInfoPanel > div:nth-child(7) > div'); // try to copy empty value const clipboard = await page.evaluate(async () => { From 43755bccca84c766f65af6ae5697627a18644e73 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Fri, 9 Jan 2026 14:31:28 +0100 Subject: [PATCH 08/12] fix: when run number is null show no link to bookkeeping --- QualityControl/public/common/object/objectInfoCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/common/object/objectInfoCard.js b/QualityControl/public/common/object/objectInfoCard.js index 9a025ce6b..2030fc06a 100644 --- a/QualityControl/public/common/object/objectInfoCard.js +++ b/QualityControl/public/common/object/objectInfoCard.js @@ -72,7 +72,7 @@ const infoRow = (key, value, infoRowAttributes) => { hasValue && infoRowAttributes(formattedKey, formattedValue), formattedValue, ), - model.services.status.isConfigured('bookkeeping') && key === 'runNumber' + model.services.status.isConfigured('bookkeeping') && key === 'runNumber' && hasValue ? h('a.ph2.text-right.actionable-icon.pointer-events-auto', { title: 'Open run in Bookkeeping', href: model.services.status.buildBookkeepingUrl(value), From df406f19e977a48ae3a2ebf655494f60332912c6 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Fri, 9 Jan 2026 14:44:10 +0100 Subject: [PATCH 09/12] test: fix timing issue introduced with changes --- QualityControl/test/public/features/filterTest.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/QualityControl/test/public/features/filterTest.test.js b/QualityControl/test/public/features/filterTest.test.js index 23c34d1df..37e27dc01 100644 --- a/QualityControl/test/public/features/filterTest.test.js +++ b/QualityControl/test/public/features/filterTest.test.js @@ -258,6 +258,7 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { await page.locator('tr:last-of-type td').click(); await page.waitForSelector(versionsPath); + await delay(100); let versionCount = await page.evaluate((path) => document.querySelectorAll(path).length, versionsPath); strictEqual(versionCount, 1, 'Number of versions is not 1'); From f96fda1b5bcabaefc8c2fe4bb16268b68312564d Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Fri, 9 Jan 2026 15:03:43 +0100 Subject: [PATCH 10/12] feat: add id to bookkeeping link for easier testing --- .../public/common/object/objectInfoCard.js | 3 ++- .../test/public/pages/object-tree.test.js | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/QualityControl/public/common/object/objectInfoCard.js b/QualityControl/public/common/object/objectInfoCard.js index 2030fc06a..95e2b38b6 100644 --- a/QualityControl/public/common/object/objectInfoCard.js +++ b/QualityControl/public/common/object/objectInfoCard.js @@ -73,7 +73,8 @@ const infoRow = (key, value, infoRowAttributes) => { formattedValue, ), model.services.status.isConfigured('bookkeeping') && key === 'runNumber' && hasValue - ? h('a.ph2.text-right.actionable-icon.pointer-events-auto', { + ? h('a.ph2.text-right.actionable-icon', { + id: 'openRunInBookkeeping', title: 'Open run in Bookkeeping', href: model.services.status.buildBookkeepingUrl(value), target: '_blank', diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index a4b3a14bc..612e60293 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -221,6 +221,24 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) } ); + await testParent.test( + 'should have a link next to the run number', + { timeout }, + async () => { + const selector = '#openRunInBookkeeping'; + + const { href, runNumber } = await page.evaluate((sel) => { + const link = document.querySelector(sel); + if (!link) return null; + + return { + href: link.getAttribute('href'), + runNumber: link.previousElementSibling?.textContent + }; + }, selector); + }, + ); + await testParent.test( 'should close the object plot upon clicking the close button', { timeout }, From 1b4ee22c6a4b82421df3070ac6c6004b30110686 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Fri, 9 Jan 2026 15:12:10 +0100 Subject: [PATCH 11/12] test: add tests for the backend and frontend --- .../lib/controllers/StatusController.test.js | 26 +++++++++++++++++++ .../test/lib/services/StatusService.test.js | 21 +++++++++++++++ .../test/public/pages/object-tree.test.js | 25 +++++++++--------- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/QualityControl/test/lib/controllers/StatusController.test.js b/QualityControl/test/lib/controllers/StatusController.test.js index 7fa073f27..70bdd7c09 100644 --- a/QualityControl/test/lib/controllers/StatusController.test.js +++ b/QualityControl/test/lib/controllers/StatusController.test.js @@ -17,6 +17,7 @@ import { ok } from 'node:assert'; import { suite, test } from 'node:test'; import { StatusController } from './../../../lib/controllers/StatusController.js'; +import { config } from '../../config.js'; export const statusControllerTestSuite = async () => { suite('`getSetServiceStatusHandler()` tests', () => { @@ -91,4 +92,29 @@ export const statusControllerTestSuite = async () => { ok(res.json.calledWith(result)); }); }); + + suite('`getServicesConfigurationHandler()` tests', () => { + test('should successfully respond with result JSON with the configured services', () => { + const mock = { + bookkeeping: { + BASE_URL: config.bookkeeping.url, + PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=', + }, + }; + + const statusService = { + retrieveServicesConfiguration: stub().returns(mock), + }; + const statusController = new StatusController(statusService); + const res = { + status: stub().returnsThis(), + json: stub(), + }; + statusController.getServicesConfigurationHandler({}, res); + + ok(statusService.retrieveServicesConfiguration.calledOnce, 'Service method should be called once'); + ok(res.status.calledWith(200), 'Response status should be 200'); + ok(res.json.calledWith(mock), 'Response JSON should match the service output'); + }); + }); }; diff --git a/QualityControl/test/lib/services/StatusService.test.js b/QualityControl/test/lib/services/StatusService.test.js index 0bec75015..5f7263fc1 100644 --- a/QualityControl/test/lib/services/StatusService.test.js +++ b/QualityControl/test/lib/services/StatusService.test.js @@ -17,6 +17,7 @@ import { deepStrictEqual } from 'node:assert'; import { suite, test, before } from 'node:test'; import { StatusService } from './../../../lib/services/Status.service.js'; +import { config } from '../../config.js'; export const statusServiceTestSuite = async () => { suite('`retrieveDataServiceStatus()` tests', () => { @@ -100,4 +101,24 @@ export const statusServiceTestSuite = async () => { }); }); }); + + suite('`retrieveServicesConfiguration()` tests', () => { + test('should return bookkeeping configuration if bookkeeping service is active', () => { + const serviceConfig = { + bookkeeping: { url: config.bookkeeping.url }, + }; + const statusService = new StatusService({ version: '0.1.1' }, serviceConfig); + + statusService.bookkeepingService = { active: true }; + + const result = statusService.retrieveServicesConfiguration(); + + deepStrictEqual(result, { + bookkeeping: { + BASE_URL: config.bookkeeping.url, + PARTIAL_RUN_DETAILS: '?page=run-detail&runNumber=', + }, + }); + }); + }); }; diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 612e60293..489f0b6b2 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -15,6 +15,7 @@ import { strictEqual, ok, deepStrictEqual, notDeepStrictEqual } from 'node:asser import { delay } from '../../testUtils/delay.js'; import { getLocalStorage, getLocalStorageAsJson } from '../../testUtils/localStorage.js'; import { StorageKeysEnum } from '../../../public/common/enums/storageKeys.enum.js'; +import { config } from '../../config.js'; const OBJECT_TREE_PAGE_PARAM = '?page=objectTree'; const SORTING_BUTTON_PATH = 'header > div > div > div:nth-child(3) > div > button'; @@ -222,22 +223,22 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) ); await testParent.test( - 'should have a link next to the run number', + 'should have an external link to bookkeeping inline with the run number row', { timeout }, async () => { - const selector = '#openRunInBookkeeping'; + const bookkeepingLink = await page.$('#openRunInBookkeeping'); + ok(bookkeepingLink, 'The link to bookkeeping should be present in the DOM'); - const { href, runNumber } = await page.evaluate((sel) => { - const link = document.querySelector(sel); - if (!link) return null; + const href = await page.evaluate((element) => element.href, bookkeepingLink); + const runNumber = + await page.evaluate((element) => element.parentElement.children[0].textContent, bookkeepingLink); + const url = new URL(href); + const baseUrl = `${url.origin}${url.pathname}`; - return { - href: link.getAttribute('href'), - runNumber: link.previousElementSibling?.textContent - }; - }, selector); - }, - ); + strictEqual(baseUrl, `${config.bookkeeping.url}/`); + strictEqual(runNumber, url.searchParams.get('runNumber')) + } + ) await testParent.test( 'should close the object plot upon clicking the close button', From beb4eded84448a02929787b9d47fa2dde8fd34ff Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Tue, 13 Jan 2026 11:57:30 +0100 Subject: [PATCH 12/12] style: add missing semicolon --- QualityControl/test/public/pages/object-tree.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 489f0b6b2..bc02b7e83 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -236,7 +236,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) const baseUrl = `${url.origin}${url.pathname}`; strictEqual(baseUrl, `${config.bookkeeping.url}/`); - strictEqual(runNumber, url.searchParams.get('runNumber')) + strictEqual(runNumber, url.searchParams.get('runNumber')); } )