From d3e389ac932c2ce554601c5103e91ca99312536f Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:47:08 +0100 Subject: [PATCH 1/2] [O2B-1546] Add and adjust synchronous QC Flags active columns and tests Introduce a dedicated synchronousQcFlagsActiveColumns config. Make formatQcFlagStart/End accept an inline flag and pass it through to formatTimestamp so From/To and Created views can render date/time on one line. Adjust a seeder row to mark a QC flag as deleted so it's text colour can be tested. Update synchronous overview tests to validate combined From/To and CreatedBy cell contents, check comment/createdBy popovers, and assert Deleted cell text and styling. --- .../seeders/20240404100811-qc-flags.js | 2 +- .../synchronousQcFlagsActiveColumns.js | 61 +++++++++++++++++++ .../SynchronousQcFlagsOverviewPage.js | 16 +---- .../views/QcFlags/format/formatQcFlagEnd.js | 5 +- .../views/QcFlags/format/formatQcFlagStart.js | 5 +- .../qcFlags/synchronousOverview.test.js | 46 ++++++++++++-- 6 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js diff --git a/lib/database/seeders/20240404100811-qc-flags.js b/lib/database/seeders/20240404100811-qc-flags.js index b66ca15bce..8f9d7d5dec 100644 --- a/lib/database/seeders/20240404100811-qc-flags.js +++ b/lib/database/seeders/20240404100811-qc-flags.js @@ -253,7 +253,7 @@ module.exports = { // Run : 56, FT0 { id: 100, - deleted: false, + deleted: true, from: null, to: '2019-08-08 20:50:00', comment: 'first part good', diff --git a/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js b/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js new file mode 100644 index 0000000000..e087e2b780 --- /dev/null +++ b/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js @@ -0,0 +1,61 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * 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 { h } from '/js/src/index.js'; +import { qcFlagsActiveColumns } from './qcFlagsActiveColumns.js'; +import { formatQcFlagStart } from '../format/formatQcFlagStart.js'; +import { formatQcFlagEnd } from '../format/formatQcFlagEnd.js'; +import { formatQcFlagCreatedBy } from '../format/formatQcFlagCreatedBy.js'; +import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; + +/** + * Active columns configuration for synchronous QC flags table + */ +export const synchronousQcFlagsActiveColumns = { + id: { + name: 'Id', + visible: false, + }, + flagType: { + ...qcFlagsActiveColumns.flagType, + classes: 'w-15', + }, + from: { + name: 'From/To', + visible: true, + format: (_, qcFlag) => h('', [ + h('.flex-row', ['From: ', formatQcFlagStart(qcFlag, true)]), + h('.flex-row', ['To: ', formatQcFlagEnd(qcFlag, true)]), + ]), + classes: 'w-15', + }, + comment: { + ...qcFlagsActiveColumns.comment, + balloon: true, + }, + deleted: { + name: 'Deleted', + visible: true, + classes: 'w-5', + format: (deleted) => deleted ? h('.danger', 'Yes') : 'No', + }, + createdBy: { + name: 'Created', + visible: true, + balloon: true, + format: (_, qcFlag) => h('', [ + h('.flex-row', ['By: ', formatQcFlagCreatedBy(qcFlag)]), + h('.flex-row', ['At: ', formatTimestamp(qcFlag.createdAt)]), + ]), + }, +}; diff --git a/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js b/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js index b8937c51ba..8fa5058b25 100644 --- a/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js +++ b/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js @@ -16,7 +16,7 @@ import { h } from '/js/src/index.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { table } from '../../../components/common/table/table.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; -import { qcFlagsActiveColumns } from '../ActiveColumns/qcFlagsActiveColumns.js'; +import { synchronousQcFlagsActiveColumns } from '../ActiveColumns/synchronousQcFlagsActiveColumns.js'; import { qcFlagsBreadcrumbs } from '../../../components/qcFlags/qcFlagsBreadcrumbs.js'; import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js'; import errorAlert from '../../../components/common/errorAlert.js'; @@ -46,16 +46,6 @@ export const SynchronousQcFlagsOverviewPage = ({ qcFlags: { synchronousOverviewM PAGE_USED_HEIGHT, )); - const activeColumns = { - qcFlagId: { - name: 'Id', - visible: false, - classes: 'w-5', - }, - ...qcFlagsActiveColumns, - }; - delete activeColumns.verified; - return h( '', { onremove: () => synchronousOverviewModel.reset() }, @@ -70,8 +60,8 @@ export const SynchronousQcFlagsOverviewPage = ({ qcFlags: { synchronousOverviewM h('.w-100.flex-column', [ table( qcFlags, - activeColumns, - { classes: '.table-sm' }, + synchronousQcFlagsActiveColumns, + { classes: '.table-sm.f6' }, null, { sort: sortModel }, ), diff --git a/lib/public/views/QcFlags/format/formatQcFlagEnd.js b/lib/public/views/QcFlags/format/formatQcFlagEnd.js index dac1426802..9cb2a7857d 100644 --- a/lib/public/views/QcFlags/format/formatQcFlagEnd.js +++ b/lib/public/views/QcFlags/format/formatQcFlagEnd.js @@ -17,11 +17,12 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j * Format QC flag `to` timestamp * * @param {QcFlag} qcFlag QC flag + * @param {boolean} inline if true, date and time are on a single line * @return {Component} formatted `to` timestamp */ -export const formatQcFlagEnd = ({ from, to }) => { +export const formatQcFlagEnd = ({ from, to }, inline = false) => { if (to) { - return formatTimestamp(to, false); + return formatTimestamp(to, inline); } else { return from ? 'Until run end' diff --git a/lib/public/views/QcFlags/format/formatQcFlagStart.js b/lib/public/views/QcFlags/format/formatQcFlagStart.js index b5a11b9b6d..bf9e8ccae5 100644 --- a/lib/public/views/QcFlags/format/formatQcFlagStart.js +++ b/lib/public/views/QcFlags/format/formatQcFlagStart.js @@ -17,11 +17,12 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j * Format QC flag `from` timestamp * * @param {QcFlag} qcFlag QC flag + * @param {boolean} inline if true, date and time are on a single line * @return {Component} formatted `from` timestamp */ -export const formatQcFlagStart = ({ from, to }) => { +export const formatQcFlagStart = ({ from, to }, inline = false) => { if (from) { - return formatTimestamp(from, false); + return formatTimestamp(from, inline); } else { return to ? 'Since run start' diff --git a/test/public/qcFlags/synchronousOverview.test.js b/test/public/qcFlags/synchronousOverview.test.js index e72c4eca91..dca5243431 100644 --- a/test/public/qcFlags/synchronousOverview.test.js +++ b/test/public/qcFlags/synchronousOverview.test.js @@ -22,6 +22,7 @@ const { expectUrlParams, waitForNavigation, getColumnCellsInnerTexts, + getPopoverContent, } = require('../defaults.js'); const { expect } = chai; @@ -59,14 +60,21 @@ module.exports = () => { it('shows correct datatypes in respective columns', async () => { // eslint-disable-next-line require-jsdoc - const validateDate = (date) => date === '-' || !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY hh:mm:ss')); + const validateDate = (date) => date === '-' || !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY, hh:mm:ss')); const tableDataValidators = { flagType: (flagType) => flagType && flagType !== '-', - createdBy: (userName) => userName && userName !== '-', - from: (timestamp) => timestamp === 'Whole run coverage' || timestamp === 'Since run start' || validateDate(timestamp), - to: (timestamp) => timestamp === 'Whole run coverage' || timestamp === 'Until run end' || validateDate(timestamp), - createdAt: validateDate, - updatedAt: validateDate, + from: (cellContent) => { + const match = cellContent.match(/^From:\s*(.+)\nTo:\s*(.+)$/); + if (!match) return false; + const [, from, to] = match; + return (['Whole run coverage', 'Since run start'].includes(from) || validateDate(from)) + && (['Whole run coverage', 'Until run end'].includes(to) || validateDate(to)); + }, + deleted: (value) => value === 'Yes' || value === 'No', + createdBy: (cellContent) => { + const match = cellContent.match(/^By:\s*(.+)\nAt:\s*(.+)$/); + return match && match[1] !== '-' && validateDate(match[2]); + }, }; await validateTableData(page, new Map(Object.entries(tableDataValidators))); @@ -80,6 +88,32 @@ module.exports = () => { await expectInnerText(page, '#totalRowsCount', '2'); }); + it('should display Comment tooltip with full information', async () => { + let popoverTrigger = await page.$(`#row100-comment .popover-trigger`); + expect(popoverTrigger).to.not.be.null; + + const popoverContent = await getPopoverContent(popoverTrigger); + expect(popoverContent).to.equal('first part good'); + }); + + it('should display CreatedBy tooltip with full information', async () => { + let popoverTrigger = await page.$(`#row100-createdBy .popover-trigger`); + expect(popoverTrigger).to.not.be.null; + + const popoverContent = await getPopoverContent(popoverTrigger); + expect(popoverContent).to.equal('By: Jan JansenAt: 12/08/2024, 12:00:00'); + }); + + it('should display correct Deleted text colour', async () => { + const deletedCell = await page.$('#row100-deleted-text:nth-child(1)'); + + const deletedCellText = await page.evaluate(cell => cell.textContent.trim(), deletedCell); + expect(deletedCellText).to.equal('Yes'); + + const deletedCellFirstChildClass = await page.evaluate(cell => cell.firstElementChild.className, deletedCell); + expect(deletedCellFirstChildClass).to.include('danger'); + }); + it('can navigate to run details page from breadcrumbs link', async () => { await waitForNavigation(page, () => pressElement(page, '#breadcrumb-run-number')); expectUrlParams(page, { page: 'run-detail', runNumber: '56' }); From 25392db61623e4eb8fb52eab6ec155523dea7bb3 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:35:02 +0100 Subject: [PATCH 2/2] [O2B-1546] Added deleted QC flag, adjust seed file and tests Added a new deleted QC flag (id 103). Update API, service and public tests to account for the additional flag and adjust selectors and counters in the UI test. Also fix several test title typos. --- .../seeders/20240404100811-qc-flags.js | 23 ++++++++++++++++++- test/api/qcFlags.test.js | 10 ++++---- .../qualityControlFlag/QcFlagService.test.js | 16 ++++++------- .../qcFlags/synchronousOverview.test.js | 6 ++--- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/lib/database/seeders/20240404100811-qc-flags.js b/lib/database/seeders/20240404100811-qc-flags.js index 8f9d7d5dec..560cb644bc 100644 --- a/lib/database/seeders/20240404100811-qc-flags.js +++ b/lib/database/seeders/20240404100811-qc-flags.js @@ -253,7 +253,7 @@ module.exports = { // Run : 56, FT0 { id: 100, - deleted: true, + deleted: false, from: null, to: '2019-08-08 20:50:00', comment: 'first part good', @@ -281,6 +281,21 @@ module.exports = { created_at: '2024-08-12 12:00:10', updated_at: '2024-08-12 12:00:10', }, + { + id: 103, + deleted: true, + from: null, + to: '2019-08-08 20:50:00', + comment: 'deleted flag', + + run_number: 56, + flag_type_id: 13, // Bad + created_by_id: 2, + detector_id: 7, // FT0 + + created_at: '2024-08-12 12:00:15', + updated_at: '2024-08-12 12:00:15', + }, // Run : 56, ITS { @@ -394,6 +409,12 @@ module.exports = { from: '2019-08-08 20:50:00', to: null, }, + { + id: 103, + flag_id: 103, + from: null, + to: '2019-08-08 20:50:00', + }, // Run : 56, ITS { diff --git a/test/api/qcFlags.test.js b/test/api/qcFlags.test.js index ee67961439..ece5d54c54 100644 --- a/test/api/qcFlags.test.js +++ b/test/api/qcFlags.test.js @@ -743,8 +743,8 @@ module.exports = () => { const response = await request(server).get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}`); expect(response.status).to.be.equal(200); const { data: flags, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 2, pageCount: 1 } }); - expect(flags.map(({ id }) => id)).to.have.all.ordered.members([101, 100]); + expect(meta).to.be.eql({ page: { totalCount: 3, pageCount: 1 } }); + expect(flags.map(({ id }) => id)).to.have.all.ordered.members([103, 101, 100]); }); it('should successfully fetch synchronous flags with pagination', async () => { @@ -752,11 +752,11 @@ module.exports = () => { const detectorId = 7; { const response = await request(server) - .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&page[limit]=1&page[offset]=1`); + .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&page[limit]=1&page[offset]=2`); expect(response.status).to.be.equal(200); const { data: flags, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 2, pageCount: 2 } }); + expect(meta).to.be.eql({ page: { totalCount: 3, pageCount: 3 } }); expect(flags).to.be.lengthOf(1); const [flag] = flags; expect(flag.id).to.be.equal(100); @@ -770,7 +770,7 @@ module.exports = () => { { const response = await request(server) .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&filter[createdBy][names]=Jan%20Jansen&filter[createdBy][operator]=or`); - expect(response.body.data).to.be.lengthOf(2); + expect(response.body.data).to.be.lengthOf(3); } { diff --git a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js index f4c533444f..3aa4300ab4 100644 --- a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js +++ b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js @@ -142,15 +142,15 @@ module.exports = () => { const detectorId = 7; { const { rows: flags, count } = await qcFlagService.getAllSynchronousPerRunAndDetector({ runNumber, detectorId }); - expect(count).to.be.equal(2); - expect(flags.map(({ id }) => id)).to.have.all.ordered.members([101, 100]); + expect(count).to.be.equal(3); + expect(flags.map(({ id }) => id)).to.have.all.ordered.members([103, 101, 100]); } { const { rows: flags, count } = await qcFlagService.getAllSynchronousPerRunAndDetector( { runNumber, detectorId }, - { limit: 1, offset: 1 }, + { limit: 1, offset: 2 }, ); - expect(count).to.be.equal(2); + expect(count).to.be.equal(3); expect(flags).to.be.lengthOf(1); const [flag] = flags; expect(flag.id).to.be.equal(100); @@ -2124,10 +2124,10 @@ module.exports = () => { }); }); - it('should successfult fiter sync flags by created by name', async () => { + it('should successfully filter sync flags by created by name', async () => { { const { rows } = await qcFlagService.getAllSynchronousPerRunAndDetector({ runNumber: 56, detectorId: 7 }, {}, { createdBy: { names: ['Jan Jansen'], operator: 'or' }}); - expect(rows).to.be.lengthOf(2); + expect(rows).to.be.lengthOf(3); } { @@ -2136,7 +2136,7 @@ module.exports = () => { } }); - it('should successfult fiter data pass flags by created by name', async () => { + it('should successfully filter data pass flags by created by name', async () => { { const { rows } = await qcFlagService.getAllPerDataPassAndRunAndDetector({ dataPassId: 1, runNumber: 107, detectorId: 1 }, {}, { createdBy: { names: ['John Doe'], operator: 'or' }}); expect(rows).to.be.lengthOf(2); @@ -2148,7 +2148,7 @@ module.exports = () => { } }); - it('should successfult fiter simulation pass flags by created by name', async () => { + it('should successfully filter simulation pass flags by created by name', async () => { { const { rows } = await qcFlagService.getAllPerSimulationPassAndRunAndDetector({ simulationPassId: 1, runNumber: 106, detectorId: 1 }, {}, { createdBy: { names: ['Jan Jansen'], operator: 'or' }}); expect(rows).to.be.lengthOf(2); diff --git a/test/public/qcFlags/synchronousOverview.test.js b/test/public/qcFlags/synchronousOverview.test.js index dca5243431..16c2900904 100644 --- a/test/public/qcFlags/synchronousOverview.test.js +++ b/test/public/qcFlags/synchronousOverview.test.js @@ -84,8 +84,8 @@ module.exports = () => { it('Should display the correct items counter at the bottom of the page', async () => { await expectInnerText(page, '#firstRowIndex', '1'); - await expectInnerText(page, '#lastRowIndex', '2'); - await expectInnerText(page, '#totalRowsCount', '2'); + await expectInnerText(page, '#lastRowIndex', '3'); + await expectInnerText(page, '#totalRowsCount', '3'); }); it('should display Comment tooltip with full information', async () => { @@ -105,7 +105,7 @@ module.exports = () => { }); it('should display correct Deleted text colour', async () => { - const deletedCell = await page.$('#row100-deleted-text:nth-child(1)'); + const deletedCell = await page.$('#row103-deleted-text:nth-child(1)'); const deletedCellText = await page.evaluate(cell => cell.textContent.trim(), deletedCell); expect(deletedCellText).to.equal('Yes');