diff --git a/QualityControl/common/library/typedef/DataPass.js b/QualityControl/common/library/typedef/DataPass.js new file mode 100644 index 000000000..3ddad2eb1 --- /dev/null +++ b/QualityControl/common/library/typedef/DataPass.js @@ -0,0 +1,18 @@ +/** + * @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. + */ + +/** + * @typedef {object} DataPass + * @property {string} name - Human-readable data pass name. + * @property {boolean} isFrozen - Whether the data pass is frozen. + */ diff --git a/QualityControl/lib/QCModel.js b/QualityControl/lib/QCModel.js index 172be0002..b057be390 100644 --- a/QualityControl/lib/QCModel.js +++ b/QualityControl/lib/QCModel.js @@ -172,4 +172,11 @@ function initializeIntervals(intervalsService, qcObjectService, filterService, r runModeService.refreshInterval, ); } + + if (filterService.dataPassesRefreshInterval > 0) { + intervalsService.register( + filterService.getDataPasses.bind(runModeService), + filterService.dataPassesRefreshInterval, + ); + } } diff --git a/QualityControl/lib/controllers/FilterController.js b/QualityControl/lib/controllers/FilterController.js index 0c7f818f2..c9b6d4526 100644 --- a/QualityControl/lib/controllers/FilterController.js +++ b/QualityControl/lib/controllers/FilterController.js @@ -65,9 +65,11 @@ export class FilterController { try { const runTypes = this._filterService?.runTypes ?? []; const detectors = this._filterService?.detectors ?? []; + const dataPasses = this._filterService?.dataPasses ?? []; res.status(200).json({ runTypes, detectors, + dataPasses, }); } catch (error) { res.status(503).json({ error: error.message || error }); diff --git a/QualityControl/lib/services/BookkeepingService.js b/QualityControl/lib/services/BookkeepingService.js index d9b0313e9..493979c6d 100644 --- a/QualityControl/lib/services/BookkeepingService.js +++ b/QualityControl/lib/services/BookkeepingService.js @@ -21,6 +21,7 @@ const GET_BKP_DATABASE_STATUS_PATH = '/api/status/database'; const GET_RUN_TYPES_PATH = '/api/runTypes'; const GET_RUN_PATH = '/api/runs'; export const GET_DETECTORS_PATH = '/api/detectors'; +const GET_DATA_PASSES_PATH = '/api/dataPasses'; const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/bkp-service`; @@ -128,6 +129,23 @@ export class BookkeepingService { return data; } + /** + * Retrieve the list of data passes from the bookkeeping service. + * @returns {Promise} Resolves with an array of data passes. + */ + async retrieveDataPasses() { + const { data } = await httpGetJson( + this._hostname, + this._port, + this._createPath(GET_DATA_PASSES_PATH), + { + protocol: this._protocol, + rejectUnauthorized: false, + }, + ); + return Array.isArray(data) ? data : []; + } + /** * Retrieves the information of a specific run from the Bookkeeping service * @param {number} runNumber - The run number to check the status for diff --git a/QualityControl/lib/services/FilterService.js b/QualityControl/lib/services/FilterService.js index 446b2d760..655c12f37 100644 --- a/QualityControl/lib/services/FilterService.js +++ b/QualityControl/lib/services/FilterService.js @@ -32,9 +32,12 @@ export class FilterService { this._bookkeepingService = bookkeepingService; this._runTypes = []; this._detectors = Object.freeze([]); + this._dataPasses = Object.freeze([]); this._runTypesRefreshInterval = config?.bookkeeping?.runTypesRefreshInterval ?? - (config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1); + (config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1);// default interval is 1 day + this._dataPassesRefreshInterval = config?.bookkeeping?.dataPassesRefreshInterval ?? + (config?.bookkeeping ? 6 * 60 * 60 * 1000 : -1);// default interval is 6 hour this.initFilters().catch((error) => { this._logger.errorMessage(`FilterService initialization failed: ${error.message || error}`); @@ -49,6 +52,7 @@ export class FilterService { await this._bookkeepingService.connect(); await this.getRunTypes(); await this._initializeDetectors(); + await this.getDataPasses(); } /** @@ -72,6 +76,33 @@ export class FilterService { } } + /** + * This method is used to retrieve the list of data passes from the bookkeeping service. + * @returns {Promise} Resolves when the list of data passes is available. + */ + async getDataPasses() { + try { + if (!this._bookkeepingService.active) { + return; + } + + const rawDataPasses = await this._bookkeepingService.retrieveDataPasses(); + this._dataPasses = Object.freeze(rawDataPasses + .filter(({ name, isFrozen }) => name && typeof isFrozen === 'boolean') + .map(({ name, isFrozen }) => Object.freeze({ name, isFrozen }))); + } catch (error) { + this._logger.errorMessage(`Error while retrieving data passes: ${error.message || error}`); + } + } + + /** + * Returns a list of data passes. + * @returns {Readonly} An immutable array of data passes. + */ + get dataPasses() { + return this._dataPasses; + } + /** * This method is used to retrieve the list of detectors from the bookkeeping service * @returns {Promise} Resolves when the list of detectors is available @@ -106,6 +137,14 @@ export class FilterService { return this._runTypesRefreshInterval; } + /** + * Returns the interval in milliseconds for how often the list of data passes should be refreshed. + * @returns {number} Interval in milliseconds for refreshing the list of data passes. + */ + get dataPassesRefreshInterval() { + return this._dataPassesRefreshInterval; + } + /** * This method is used to initialize the filter service * @returns {string[]} - resolves when the filter service is initialized diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index dbb79c554..b92ea7df6 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -13,7 +13,7 @@ */ import { FilterType } from './filterTypes.js'; -import { h, RemoteData } from '/js/src/index.js'; +import { h, RemoteData, DropdownComponent } from '/js/src/index.js'; /** * Builds a filter element. If options to show, selector filter element; otherwise, input element. @@ -136,6 +136,73 @@ export const groupedDropdownComponent = ({ ]); }; +/** + * Builds a filter element. If options to show, selector filter element; otherwise, input element. + * @param {object} config - Configuration object for building the filter element. + * @param {string} config.queryLabel - The key used to query the storage with this parameter. + * @param {string} config.placeholder - The placeholder text to be displayed in the input field. + * @param {string} config.id - The unique identifier for the input field. + * @param {object} config.filterMap - Map of the current filter values. + * @param {string} [config.type='text'] - The type of the filter element (e.g., 'text', 'number'). + * @param {Record} [config.options={}] - List of options for an input with dropdown selector (optional). + * @param {(filterId: string, value: string, setUrl: boolean) => void} config.onChangeCallback + * - Callback to be triggered on the change event of the filter. + * @param {(filterId: string, value: string, setUrl: boolean) => void} config.onInputCallback + * - Callback to be triggered on the input event. + * @param {(filterId: string, value: string, setUrl: boolean) => void} config.onEnterCallback + * - Callback to be triggered when the Enter key is pressed. + * @param {string} [config.width='.w-20'] - The CSS class that defines the width of the filter. + * @returns {vnode} A virtual node element representing the filter element. + */ +export const inputWithDropdownComponent = ({ + queryLabel, + placeholder, + id, + filterMap, + options = {}, + onChangeCallback, + onInputCallback, + onEnterCallback, + type = 'text', + width = '.w-20', +}) => { + const dropdownOptions = Object.keys(options); + if (!dropdownOptions.length) { + return filterInput({ queryLabel, placeholder, id, filterMap, onInputCallback, onEnterCallback, type, width }); + } + const dropdownComponent = DropdownComponent( + filterInput({ + queryLabel, + placeholder, + id, + filterMap, + type, + onInputCallback, + onEnterCallback, + width: '.w-100', + }), + h('', { + id: `${queryLabel?.toLowerCase()}-dropdown`, + style: 'max-height: 300px; overflow-y: auto;', + }, Object.entries(options) + .filter(([option]) => option.toLowerCase().includes(filterMap[queryLabel]?.toLowerCase() ?? '')) + .sort(([a], [b]) => b.localeCompare(a)) + .map(([option, htmlOptions]) => h( + 'button.btn.d-block.w-100', + { + onclick: () => { + onChangeCallback(queryLabel, option, true); + dropdownComponent.state.hidePopover(); + }, + ...htmlOptions ?? {}, + }, + [option, Object.keys(htmlOptions).length > 0 ? ' (frozen)' : ''], + ))), + ); + + return h(`${width}`, dropdownComponent); +}; + /** * Builds a filter input element that allows the user to specify a parameter to be used when querying objects. * This function renders a text input element with event handling for input and Enter key press. diff --git a/QualityControl/public/common/filters/filterTypes.js b/QualityControl/public/common/filters/filterTypes.js index b96b67e58..0ac837bf3 100644 --- a/QualityControl/public/common/filters/filterTypes.js +++ b/QualityControl/public/common/filters/filterTypes.js @@ -14,6 +14,7 @@ const FilterType = { INPUT: 'input', + INPUT_WITH_DROPDOWN: 'inputWithDropdown', DROPDOWN: 'dropdownSelector', GROUPED_DROPDOWN: 'groupedDropdownSelector', RUN_MODE: 'runModeSelector', diff --git a/QualityControl/public/common/filters/filterViews.js b/QualityControl/public/common/filters/filterViews.js index ac3bcf6ec..486968a5d 100644 --- a/QualityControl/public/common/filters/filterViews.js +++ b/QualityControl/public/common/filters/filterViews.js @@ -17,6 +17,7 @@ import { dynamicSelector, ongoingRunsSelector, groupedDropdownComponent, + inputWithDropdownComponent, } from './filter.js'; import { FilterType } from './filterTypes.js'; import { filtersConfig, runModeFilterConfig } from './filtersConfig.js'; @@ -57,6 +58,8 @@ const createFilterElement = return dynamicSelector({ ...commonConfig, options, onChangeCallback, inputType }); case FilterType.GROUPED_DROPDOWN: return groupedDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType }); + case FilterType.INPUT_WITH_DROPDOWN: + return inputWithDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType }); case FilterType.RUN_MODE: return ongoingRunsSelector( { ...commonConfig }, diff --git a/QualityControl/public/common/filters/filtersConfig.js b/QualityControl/public/common/filters/filtersConfig.js index 751c52d93..45c2f9e75 100644 --- a/QualityControl/public/common/filters/filtersConfig.js +++ b/QualityControl/public/common/filters/filtersConfig.js @@ -17,11 +17,12 @@ import { FilterType } from './filterTypes.js'; /** * Returns an array of filter configuration objects used to render dynamic filter inputs. * @param {FilterService} filterService - service to get the data to populate the filters - * @param {string[]} filterService.runTypes - run types to show in the filter - * @param {DetectorSummary[]} filterService.detectors - detectors to show in the filter + * @param {RemoteData} filterService.runTypes - run types to show in the filter + * @param {RemoteData} filterService.detectors - detectors to show in the filter + * @param {RemoteData} filterService.dataPasses - data passes to show in the filter * @returns {object[]} Filter configuration array */ -export const filtersConfig = ({ runTypes, detectors }) => [ +export const filtersConfig = ({ runTypes, detectors, dataPasses }) => [ { type: FilterType.INPUT, queryLabel: 'RunNumber', @@ -59,10 +60,17 @@ export const filtersConfig = ({ runTypes, detectors }) => [ id: 'periodNameFilter', }, { - type: FilterType.INPUT, + type: FilterType.INPUT_WITH_DROPDOWN, queryLabel: 'PassName', placeholder: 'PassName (e.g. apass2)', id: 'passNameFilter', + options: dataPasses.match({ + Success: (payload) => payload.reduce((acc, dataPass) => { + acc[dataPass.name] = dataPass.isFrozen ? { style: 'color: var(--color-gray-dark);' } : {}; + return acc; + }, {}), + Other: () => {}, + }), }, { type: FilterType.INPUT, diff --git a/QualityControl/public/services/Filter.service.js b/QualityControl/public/services/Filter.service.js index 94c949c3f..0758cfaa6 100644 --- a/QualityControl/public/services/Filter.service.js +++ b/QualityControl/public/services/Filter.service.js @@ -29,6 +29,7 @@ export default class FilterService { this._runTypes = RemoteData.notAsked(); this._detectors = RemoteData.notAsked(); + this._dataPasses = RemoteData.notAsked(); this.ongoingRuns = RemoteData.notAsked(); } @@ -40,14 +41,17 @@ export default class FilterService { async getFilterConfigurations() { this._runTypes = RemoteData.loading(); this._detectors = RemoteData.loading(); + this._dataPasses = RemoteData.notAsked(); this.filterModel.notify(); const { result, ok } = await this.loader.get('/api/filter/configuration'); if (ok) { this._runTypes = RemoteData.success(result?.runTypes || []); this._detectors = RemoteData.success(result?.detectors || []); + this._dataPasses = RemoteData.success(result?.dataPasses || []); } else { this._runTypes = RemoteData.failure('Error retrieving runTypes'); this._detectors = RemoteData.failure('Error retrieving detectors'); + this._dataPasses = RemoteData.failure('Error retrieving dataPasses'); } this.filterModel.notify(); } @@ -126,4 +130,12 @@ export default class FilterService { get detectors() { return this._detectors; } + + /** + * Returns a {@link RemoteData} object containing an array of data type {@link DataPass}. + * @returns {RemoteData} A {@link RemoteData} object containing an array of data type {@link DataPass}. + */ + get dataPasses() { + return this._dataPasses; + } } diff --git a/QualityControl/test/lib/controllers/FiltersController.test.js b/QualityControl/test/lib/controllers/FiltersController.test.js index 64cd7b62a..465dec863 100644 --- a/QualityControl/test/lib/controllers/FiltersController.test.js +++ b/QualityControl/test/lib/controllers/FiltersController.test.js @@ -37,8 +37,8 @@ export const filtersControllerTestSuite = async () => { }); }); - suite('getFilterConfigurationHandler', () => { - test('should successfully retrieve run types and detectors from Bookkeeping service', async () => { + suite('getFilterConfigurationHandler', async () => { + test('should successfully retrieve run types, detectors and data passes from Bookkeeping service', async () => { const filterService = sinon.createStubInstance(FilterService); const mockedRunTypes = ['runType1', 'runType2']; const mockedDetectors = [ @@ -47,8 +47,15 @@ export const filtersControllerTestSuite = async () => { type: 'PHYSICAL', }, ]; + const mockedDataPasses = [ + { + name: 'LHC22a_apass1', + isFrozen: false, + }, + ]; sinon.stub(filterService, 'runTypes').get(() => mockedRunTypes); sinon.stub(filterService, 'detectors').get(() => mockedDetectors); + sinon.stub(filterService, 'dataPasses').get(() => mockedDataPasses); const res = { status: sinon.stub().returnsThis(), @@ -59,11 +66,11 @@ export const filtersControllerTestSuite = async () => { filterController.getFilterConfigurationHandler(req, res); ok(res.status.calledWith(200), 'Response status was not 200'); ok( - res.json.calledWith({ runTypes: mockedRunTypes, detectors: mockedDetectors }), - 'Response should include runTypes and detectors', + res.json.calledWith({ runTypes: mockedRunTypes, detectors: mockedDetectors, dataPasses: mockedDataPasses }), + 'Response should include runTypes, detectors and dataPasses', ); }); - test('should return an empty arrays if bookkeeping service is not defined', () => { + test('should return an empty arrays if bookkeeping service is not defined', async () => { const bkpService = null; const res = { status: sinon.stub().returnsThis(), @@ -74,8 +81,8 @@ export const filtersControllerTestSuite = async () => { filterController.getFilterConfigurationHandler(req, res); ok(res.status.calledWith(200), 'Response status was not 200'); ok( - res.json.calledWith({ runTypes: [], detectors: [] }), - 'runTypes and detectors were not sent as an empty array', + res.json.calledWith({ runTypes: [], detectors: [], dataPasses: [] }), + 'runTypes, detectors and dataPasses were not sent as an empty array', ); }); }); diff --git a/QualityControl/test/lib/services/BookkeepingService.test.js b/QualityControl/test/lib/services/BookkeepingService.test.js index d6533fad7..b8b09cb52 100644 --- a/QualityControl/test/lib/services/BookkeepingService.test.js +++ b/QualityControl/test/lib/services/BookkeepingService.test.js @@ -335,6 +335,7 @@ export const bookkeepingServiceTestSuite = async () => { strictEqual(runStatus, RunStatus.BOOKKEEPING_UNAVAILABLE); }); }); + suite('Retrieve detector summaries', () => { let bkpService = null; @@ -406,5 +407,127 @@ export const bookkeepingServiceTestSuite = async () => { strictEqual(result.length, 0); }); }); + + suite('Retrieve data passes', () => { + const GET_DATA_PASSES_PATH = '/api/dataPasses'; + + let bkpService = null; + + before(() => { + bkpService = new BookkeepingService(VALID_CONFIG.bookkeeping); + bkpService.validateConfig(); // ensures internal fields like _hostname/_port/_token are set + bkpService.connect(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + test('should handle all detector types correctly', async () => { + const mockResponse = { + data: [ + { + id: 9, + name: 'LHC23f_cpass0', + isFrozen: false, + versions: [], + pdpBeamTypes: ['OO'], + runCount: 1, + simulationPassesCount: 1, + }, + { + id: 2, + name: 'LHC22b_skimming', + isFrozen: false, + versions: [], + pdpBeamTypes: ['pp'], + runCount: 2, + simulationPassesCount: 2, + }, + { + id: 5, + name: 'LHC22b_apass2_skimmed', + isFrozen: false, + versions: [], + pdpBeamTypes: ['PbPb'], + runCount: 3, + simulationPassesCount: 1, + }, + { + id: 1, + name: 'LHC22b_apass1', + isFrozen: false, + versions: [], + pdpBeamTypes: ['pp'], + runCount: 4, + simulationPassesCount: 0, + }, + { + id: 4, + name: 'LHC22a_apass2', + isFrozen: false, + versions: [], + pdpBeamTypes: ['PbPb'], + runCount: 5, + simulationPassesCount: 2, + }, + { + id: 3, + name: 'LHC22a_apass1', + isFrozen: false, + versions: [], + pdpBeamTypes: ['PbPb'], + runCount: 4, + simulationPassesCount: 0, + }, + ], + }; + + nock(VALID_CONFIG.bookkeeping.url) + .get(GET_DATA_PASSES_PATH) + .query({ token: VALID_CONFIG.bookkeeping.token }) + .reply(200, mockResponse); + + const result = await bkpService.retrieveDataPasses(); + + ok(Array.isArray(result)); + strictEqual(result.length, mockResponse.data.length); + + // Verify data passes data is preserved + deepStrictEqual(result, mockResponse.data); + }); + + test('should return empty array when data is not an array', async () => { + const mockResponse = { + data: null, + }; + + nock(VALID_CONFIG.bookkeeping.url) + .get(GET_DATA_PASSES_PATH) + .query({ token: VALID_CONFIG.bookkeeping.token }) + .reply(200, mockResponse); + + const result = await bkpService.retrieveDataPasses(); + + ok(Array.isArray(result)); + strictEqual(result.length, 0); + }); + + test('should return empty array when data is empty array', async () => { + const mockResponse = { + data: [], + }; + + nock(VALID_CONFIG.bookkeeping.url) + .get(GET_DATA_PASSES_PATH) + .query({ token: VALID_CONFIG.bookkeeping.token }) + .reply(200, mockResponse); + + const result = await bkpService.retrieveDataPasses(); + + ok(Array.isArray(result)); + strictEqual(result.length, 0); + }); + }); }); }; diff --git a/QualityControl/test/lib/services/FilterService.test.js b/QualityControl/test/lib/services/FilterService.test.js index 2bd165dec..3947c2cf4 100644 --- a/QualityControl/test/lib/services/FilterService.test.js +++ b/QualityControl/test/lib/services/FilterService.test.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { deepStrictEqual, ok } from 'node:assert'; +import { deepStrictEqual, ok, strictEqual } from 'node:assert'; import { suite, test, beforeEach, afterEach } from 'node:test'; import { FilterService } from '../../../lib/services/FilterService.js'; import { RunStatus } from '../../../common/library/runStatus.enum.js'; @@ -24,6 +24,7 @@ export const filterServiceTestSuite = async () => { const configMock = { bookkeeping: { runTypesRefreshInterval: 24 * 60 * 60 * 1000, // 24 hours + dataPassesRefreshInterval: 60 * 60 * 1000, // 1 hour }, }; @@ -33,6 +34,7 @@ export const filterServiceTestSuite = async () => { retrieveRunTypes: stub(), retrieveRunInformation: stub(), retrieveDetectorSummaries: stub(), + retrieveDataPasses: stub(), active: true, // assume the bookkeeping service is active by default }; filterService = new FilterService(bookkeepingServiceMock, configMock); @@ -58,17 +60,33 @@ export const filterServiceTestSuite = async () => { deepStrictEqual(filterServiceWithDefaultConfig._runTypesRefreshInterval, 24 * 60 * 60 * 1000); }); + test('should set data passes refresh interval to default if not provided', () => { + const filterServiceWithDefaultConfig = new FilterService(bookkeepingServiceMock, { bookkeeping: {} }); + strictEqual(filterServiceWithDefaultConfig._dataPassesRefreshInterval, 6 * 60 * 60 * 1000); + }); + test('should set run types refresh interval to the value from config', () => { const customConfig = { bookkeeping: { runTypesRefreshInterval: 5000 } }; const filterServiceWithCustomConfig = new FilterService(bookkeepingServiceMock, customConfig); deepStrictEqual(filterServiceWithCustomConfig._runTypesRefreshInterval, 5000); }); + test('should set data passes refresh interval to the value from config', () => { + const customConfig = { bookkeeping: { dataPassesRefreshInterval: 5000 } }; + const filterServiceWithCustomConfig = new FilterService(bookkeepingServiceMock, customConfig); + strictEqual(filterServiceWithCustomConfig._dataPassesRefreshInterval, 5000); + }); + test('should init _detectors on instantiation', async () => { deepStrictEqual(filterService._detectors, []); ok(Object.isFrozen(filterService._detectors)); }); + test('should init _dataPasses on instantiation', async () => { + deepStrictEqual(filterService._dataPasses, []); + ok(Object.isFrozen(filterService._dataPasses)); + }); + test('should init filters on instantiation', async () => { const initFiltersStub = stub(filterService, 'initFilters'); await filterService.initFilters(); @@ -105,6 +123,34 @@ export const filterServiceTestSuite = async () => { deepStrictEqual(filterService._detectors, [DETECTOR_SUMMARIES[0]]); ok(Object.isFrozen(filterService._detectors)); }); + + test('should call getDataPasses', async () => { + const getDataPassesStub = stub(filterService, 'getDataPasses'); + await filterService.initFilters(); + ok(getDataPassesStub.calledOnce); + }); + + test('should set _dataPasses on getDataPasses call', async () => { + const DATA_PASSES = [ + { + name: 'Data pass human-readable name 1', + isFrozen: false, + dummy: 'some dummy data that should be removed', + }, + { + name: 'Data pass human-readable name 2', + isFrozen: true, + dummy: 'some more dummy data that should be removed', + }, + ]; + + bookkeepingServiceMock.retrieveDataPasses.resolves(DATA_PASSES); + await filterService.getDataPasses(); + + const EXPECTED_DATA_PASSES = DATA_PASSES.map(({ name, isFrozen }) => ({ name, isFrozen })); + deepStrictEqual(filterService._dataPasses, EXPECTED_DATA_PASSES); + ok(Object.isFrozen(filterService._dataPasses)); + }); }); suite('getRunTypes', async () => { @@ -138,6 +184,17 @@ export const filterServiceTestSuite = async () => { }); }); + suite('dataPassesRefreshInterval', async () => { + test('should return the data passes refresh interval', () => { + strictEqual(filterService.dataPassesRefreshInterval, configMock.bookkeeping.dataPassesRefreshInterval); + }); + + test('should return -1 if bookkeeping config is not set', () => { + const filterServiceWithoutConfig = new FilterService(bookkeepingServiceMock, {}); + strictEqual(filterServiceWithoutConfig.dataPassesRefreshInterval, -1); + }); + }); + suite('runTypes', async () => { test('should return the list of run types', () => { filterService._runTypes = ['type1', 'type2']; diff --git a/QualityControl/test/public/pages/layout-show.test.js b/QualityControl/test/public/pages/layout-show.test.js index cc18f809a..69219abe2 100644 --- a/QualityControl/test/public/pages/layout-show.test.js +++ b/QualityControl/test/public/pages/layout-show.test.js @@ -437,15 +437,10 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => 'should close JSON editor when clicking "Cancel"', { timeout }, async () => { - const cancelButtonPath = 'body > div > div > div > div > button:nth-child(2)'; - await page.locator(cancelButtonPath).click(); + await page.locator('.o2-modal-content .btn-group > button:nth-child(2)').click(); await delay(50); - const childrenCount = await page.evaluate(() => { - const bodyPath = 'body'; - const body = document.querySelector(bodyPath); - return body.children.length; - }); - strictEqual(childrenCount, 2); + const isModelOpen = await page.evaluate(() => document.querySelector('.o2-modal-content') !== null); + strictEqual(isModelOpen, false); }, ); diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 5c9beac70..74b43fb2d 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -17,7 +17,6 @@ import { getLocalStorage, getLocalStorageAsJson } from '../../testUtils/localSto import { StorageKeysEnum } from '../../../public/common/enums/storageKeys.enum.js'; const OBJECT_TREE_PAGE_PARAM = '?page=objectTree'; -const SORTING_BUTTON_PATH = 'header > div > div > div:nth-child(3) > div > button'; /** * Initial page setup tests @@ -198,7 +197,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) strictEqual(clipboard, 'qc/test/object/1'); context.clearPermissionOverrides(); - } + }, ); await testParent.test( @@ -218,7 +217,7 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) strictEqual(clipboard, 'qc/test/object/1'); context.clearPermissionOverrides(); - } + }, ); await testParent.test( @@ -230,15 +229,14 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) await page.waitForFunction( (selector) => document.querySelector(selector).children.length === 1, {}, - 'section > div > div' + 'section > div > div', ); const selectedObject = await page.evaluate(() => model.object.selected); const numberOfChildren = await page.evaluate(() => - document.querySelector('section > div > div').children.length - ); + document.querySelector('section > div > div').children.length); strictEqual(selectedObject, undefined); strictEqual(numberOfChildren, 1); - } + }, ); await testParent.test('should update local storage when tree node is clicked', { timeout }, async () => { @@ -335,6 +333,57 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) }, ); + await testParent.test( + 'should have a selector with sorted options to filter by data passes if there are data passes loaded', + { timeout }, + async () => { + await page.locator('#passNameFilter').click(); + await page.waitForSelector('#passname-dropdown', { visible: true, timeout: 1000 }); + + const options = await page.evaluate(() => { + const optionElements = document.querySelectorAll('#passname-dropdown > button'); + return Array.from(optionElements).map((option) => option.textContent); + }); + + const expectedOptions = [ + 'LHC23f_cpass0', + 'LHC22b_skimming', + 'LHC22b_apass2_skimmed', + 'LHC22b_apass1', + 'LHC22a_apass2', + 'LHC22a_apass1', + ]; + + strictEqual( + options.length, + expectedOptions.length, + `PassName dropdown should have ${expectedOptions.length} options, found ${expectedOptions}`, + ); + deepStrictEqual(options, expectedOptions, 'PassName dropdown options are incorrect'); + }, + ); + + await testParent.test( + 'should update the passNameFilter input when a dropdown option is selected', + { timeout }, + async () => { + const expectedOptionValue + = await page.evaluate(() => document.querySelector('#passname-dropdown > button')?.textContent); + + await page.locator('#passname-dropdown > button').click(); + await page.waitForSelector('#passname-dropdown', { hidden: true, timeout: 1000 }); + await delay(50); + + const inputValue = await page.evaluate(() => document.querySelector('input#passNameFilter')?.value); + + strictEqual( + inputValue, + expectedOptionValue, + 'should set the input value to the clicked passName dropdown option', + ); + }, + ); + await testParent.test( 'should have a grouped selector with sorted options to filter by detector if there are detectors loaded', { timeout }, diff --git a/QualityControl/test/setup/testSetupForBkp.js b/QualityControl/test/setup/testSetupForBkp.js index c1c061d12..33a44a610 100644 --- a/QualityControl/test/setup/testSetupForBkp.js +++ b/QualityControl/test/setup/testSetupForBkp.js @@ -84,6 +84,67 @@ export const initializeNockForBkp = () => { }, ], }); + nock(BKP_URL) + .persist() + .get(`/api/dataPasses${TOKEN_PATH}`) + .reply(200, { + data: [ + { + id: 9, + name: 'LHC23f_cpass0', + isFrozen: false, + versions: [], + pdpBeamTypes: ['OO'], + runCount: 1, + simulationPassesCount: 1, + }, + { + id: 2, + name: 'LHC22b_skimming', + isFrozen: false, + versions: [], + pdpBeamTypes: ['pp'], + runCount: 2, + simulationPassesCount: 2, + }, + { + id: 5, + name: 'LHC22b_apass2_skimmed', + isFrozen: false, + versions: [], + pdpBeamTypes: ['PbPb'], + runCount: 3, + simulationPassesCount: 1, + }, + { + id: 1, + name: 'LHC22b_apass1', + isFrozen: false, + versions: [], + pdpBeamTypes: ['pp'], + runCount: 4, + simulationPassesCount: 0, + }, + { + id: 4, + name: 'LHC22a_apass2', + isFrozen: false, + versions: [], + pdpBeamTypes: ['PbPb'], + runCount: 5, + simulationPassesCount: 2, + }, + { + id: 3, + name: 'LHC22a_apass1', + isFrozen: false, + versions: [], + pdpBeamTypes: ['PbPb'], + runCount: 4, + simulationPassesCount: 0, + }, + ], + }); nock(BKP_URL) .persist() .get(`/api/runs/0${TOKEN_PATH}`)