From e981b7a0ccdf6a60c2ac907b1111627c3db894f9 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Wed, 11 Feb 2026 15:56:09 +0100 Subject: [PATCH 01/23] chore: add runfilterDTO beamMode patern --- lib/domain/dtos/filters/RunFilterDto.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 2dc9ce98ad..54240b1935 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -32,6 +32,9 @@ const EorReasonFilterDto = Joi.object({ }); exports.RunFilterDto = Joi.object({ + beamModes: Joi.string().trim().pattern(/^[A-Z]+(?: [A-Z]+)*$/).messages({ + 'string.pattern.base': 'Beam modes must contain only uppercase letters and single spaces between words.' + }), runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', From 43c811d50478a688ed0d237bf512cca8cb27358b Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Wed, 11 Feb 2026 16:02:35 +0100 Subject: [PATCH 02/23] chore: add beamModes to getAllRunsUsecase --- lib/usecases/run/GetAllRunsUseCase.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index d25762ad00..6724258098 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -82,6 +82,7 @@ class GetAllRunsUseCase { inelasticInteractionRateAtEnd, gaq, detectorsQc, + beamModes } = filter; if (runNumbers) { @@ -113,6 +114,10 @@ class GetAllRunsUseCase { filteringQueryBuilder.where('definition').oneOf(...definitions); } + if (beamModes) { + filteringQueryBuilder.where('lhcBeamMode').oneOf(...definitions); + } + if (eorReason) { const eorReasonTypeWhere = {}; if (eorReason.category) { From f1d366baa628204bef9a283a95f0f54b2bba357e Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 09:10:36 +0100 Subject: [PATCH 03/23] update dto --- lib/domain/dtos/filters/RunFilterDto.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 54240b1935..7439c07f12 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -32,9 +32,15 @@ const EorReasonFilterDto = Joi.object({ }); exports.RunFilterDto = Joi.object({ - beamModes: Joi.string().trim().pattern(/^[A-Z]+(?: [A-Z]+)*$/).messages({ - 'string.pattern.base': 'Beam modes must contain only uppercase letters and single spaces between words.' - }), + beamModes: CustomJoi.stringArray().items( + Joi.string() + .trim() + .pattern(/^[A-Z]+(?: [A-Z]+)*$/) + .messages({ + 'string.pattern.base': + 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', + }) + ), runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', From 01706582d925e045b2b9c1af6454732bc5fac776 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 09:11:09 +0100 Subject: [PATCH 04/23] use beammodes in getallRunsUseCase instead of definitions --- lib/usecases/run/GetAllRunsUseCase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index 6724258098..ee964cb21d 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -115,7 +115,7 @@ class GetAllRunsUseCase { } if (beamModes) { - filteringQueryBuilder.where('lhcBeamMode').oneOf(...definitions); + filteringQueryBuilder.where('lhcBeamMode').oneOf(...beamModes); } if (eorReason) { From 6fd6bfdccb277dbe8c43ad1a016aa6b5022652a1 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 09:45:37 +0100 Subject: [PATCH 05/23] add runs api test --- test/api/runs.test.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 4322040bb3..083771bf02 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -142,6 +142,37 @@ module.exports = () => { expect(runs).to.lengthOf(20); }); + it('should successfully filter with single beamMode', async () => { + const response = await request(server).get('/api/runs?filter[beamModes]=STABLE BEAMS'); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.lengthOf(5); + }); + + it('should successfully filter with multiple beamModes', async () => { + const response = await request(server).get('/api/runs?filter[beamModes]=STABLE BEAMS,NO BEAM'); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.lengthOf(6); + }); + + it('should return 400 if beamModes filter has the incorrect format', async () => { + const beamModeString = '*THERE\'S NON LETTERS IN HERE'; + const response = await request(server).get(`/api/runs?filter[beamModes]=${beamModeString}`); + + + expect(response.status).to.equal(400); + + const { errors: [error] } = response.body; + + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal(`Beam modes "${beamModeString}" must contain only uppercase letters and single spaces between words.`); + }); + it('should successfully filter on multiple specified run numbers', async () => { const response = await request(server).get('/api/runs?filter[runNumbers]=17,18'); From 7364379394fdf3be3ea1cf0dad00315bd143e85e Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 10:27:29 +0100 Subject: [PATCH 06/23] add usecase tests --- .../usecases/run/GetAllRunsUseCase.test.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index 55e5551485..5b080d056c 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -183,6 +183,32 @@ module.exports = () => { } }); + it('should successfully filter on beamModes', async () => { + const singleBeamMode = ['STABLE BEAMS']; + const multipleBeamMode = ['STABLE BEAMS', 'NO BEAM']; + const nonExistentBeamMode = ['DOES NOT EXIST']; + + getAllRunsDto.query = { filter: { beamModes: singleBeamMode } }; + { + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.have.lengthOf(5); + expect(runs.every(({ lhcBeamMode }) => singleBeamMode.includes(lhcBeamMode))).to.be.true; + } + + getAllRunsDto.query = { filter: { beamModes: multipleBeamMode } }; + { + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.have.lengthOf(6); + expect(runs.every(({ lhcBeamMode }) => multipleBeamMode.includes(lhcBeamMode))).to.be.true; + } + + getAllRunsDto.query = { filter: { beamModes: nonExistentBeamMode } }; + { + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.have.lengthOf(0); + } + }); + it('should successfully filter on run definition', async () => { const PHYSICS_COUNT = 7; const COSMICS_COUNT = 2; From 02128b42c8166b181f7a0d7c523d06fda84cc0cd Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 10:38:53 +0100 Subject: [PATCH 07/23] add trailing comma --- lib/usecases/run/GetAllRunsUseCase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index ee964cb21d..df1b5f7f5b 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -82,7 +82,7 @@ class GetAllRunsUseCase { inelasticInteractionRateAtEnd, gaq, detectorsQc, - beamModes + beamModes, } = filter; if (runNumbers) { From 43e5861fa5e421085c4a4d37d279e60787daf975 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 10:44:49 +0100 Subject: [PATCH 08/23] fix lint issues --- lib/domain/dtos/filters/RunFilterDto.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 7439c07f12..93f3d5e238 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -32,15 +32,13 @@ const EorReasonFilterDto = Joi.object({ }); exports.RunFilterDto = Joi.object({ - beamModes: CustomJoi.stringArray().items( - Joi.string() + beamModes: CustomJoi.stringArray().items(Joi.string() .trim() .pattern(/^[A-Z]+(?: [A-Z]+)*$/) .messages({ 'string.pattern.base': 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', - }) - ), + })), runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', From 1f30bc725b2f16d4c1bd150145b0c19a2aae021a Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 14:16:14 +0100 Subject: [PATCH 09/23] feat: create beammodesProvider --- .../services/beamModes/beamModesProvider.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 lib/public/services/beamModes/beamModesProvider.js diff --git a/lib/public/services/beamModes/beamModesProvider.js b/lib/public/services/beamModes/beamModesProvider.js new file mode 100644 index 0000000000..dde74e56d2 --- /dev/null +++ b/lib/public/services/beamModes/beamModesProvider.js @@ -0,0 +1,30 @@ +/** + * @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 { getRemoteData } from '../../utilities/fetch/getRemoteData.js'; +import { RemoteDataProvider } from '../RemoteDataProvider.js'; + +/** + * Service class to fetch beamModes from the backend + */ +export class BeamModesProvider extends RemoteDataProvider { + /** + * @inheritDoc + */ + async getRemoteData() { + const { data } = await getRemoteData('/api/runs/beamModes'); + return data; + } +} + +export const beamModesProvider = new BeamModesProvider(); From fbd3377b013d9c0b843513c849f0d0cc9f2e7e15 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 14:17:00 +0100 Subject: [PATCH 10/23] feat: create beamModeFilterModel --- .../Filters/RunsFilter/BeamModeFilterModel.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js diff --git a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js new file mode 100644 index 0000000000..19f6fb02b4 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -0,0 +1,54 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { beamModesProvider } from '../../../services/beamModes/beamModesProvider.js' +import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; +import { FilterModel } from '../common/FilterModel.js'; + +/** + * Beam mode filter model + */ +export class BeamModeFilterModel extends FilterModel { + /** + * Constructor + */ + constructor() { + super({}); + + this._dropDownModel = new ObservableBasedSelectionDropdownModel(beamModesProvider.items$, ({ name }) => ({ value: name })); + } + + /** + * @inheritDoc + */ + reset() { + this._dropDownModel.reset(); + } + + /** + * @inheritDoc + */ + get isEmpty() { + return this._dropDownModel.isEmpty; + } + + /** + * Return the underlying dropdown model + * + * @return {ObservableDropDownModel} the underlying dropdown model + */ + get dropDownModel() { + return this._dropDownModel; + } +} From eae9a552b46a43166a412a9f7e051a0db051b747 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 14:17:35 +0100 Subject: [PATCH 11/23] feat: add beamModeFilterModel to RunsoverViewModel --- lib/public/views/Runs/Overview/RunsOverviewModel.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 8b1a7ca32c..cfad9dc912 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -34,6 +34,7 @@ import { RUN_QUALITIES } from '../../../domain/enums/RunQualities.js'; import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; import { DataExportModel } from '../../../models/DataExportModel.js'; import { runsActiveColumns as dataExportConfiguration } from '../ActiveColumns/runsActiveColumns.js'; +import { BeamModeFilterModel } from '../../../components/Filters/RunsFilter/BeamModeFilterModel.js'; /** * Model representing handlers for runs page @@ -67,6 +68,7 @@ export class RunsOverviewModel extends OverviewPageModel { runDuration: new NumericalComparisonFilterModel({ scale: 60 * 1000 }), environmentIds: new RawTextFilterModel(), runTypes: new RunTypesFilterModel(runTypesProvider.items$), + beamModes: new BeamModeFilterModel(), runQualities: new SelectionFilterModel({ availableOptions: RUN_QUALITIES.map((quality) => ({ label: quality.toUpperCase(), From 6fd1b9b0341fe1bb40bbc7ef5364f8ab7c7b3c15 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 12 Feb 2026 14:17:54 +0100 Subject: [PATCH 12/23] feat: add beamModeFilterModel to the view --- .../Runs/ActiveColumns/runsActiveColumns.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 2278a39466..9beaf89503 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -150,6 +150,27 @@ export const runsActiveColumns = { filter: (runModel) => tagFilter(runModel.filteringModel.get('tags')), balloon: (tags) => tags && tags.length > 0, }, + beamModes: { + name: 'Beam Modes', + visible: true, + profiles: { + [profiles.none]: null, + lhcFill: null, + environment: null, + runsPerDataPass: { visible: false }, + }, + classes: 'w-5 f6', + exportFormat: (beamModes) => beamModes?.length ? beamModes.map(({ text }) => text).join('-') : '-', + + /** + * Beam Mode filter component + * + * @param {RunsOverviewModel} runModel the runs overview model + * @return {Component} the filter component + */ + filter: (runModel) => selectionDropdown(runModel.filteringModel.get('beamModes').dropDownModel, { selectorPrefix: 'beamMode' }), + balloon: (beamMode) => beamMode && beamMode.length > 0, + }, fillNumber: { name: 'Fill No.', visible: true, From 1e56e4f4bcdf772577971eba8b9bd960f9d649d7 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Fri, 13 Feb 2026 10:15:35 +0100 Subject: [PATCH 13/23] chore: rewrite BeamModeFilteringmodel to be based on runTypes rather than run definitions --- .../Filters/RunsFilter/BeamModeFilterModel.js | 24 ++++++++++++------- .../views/Runs/Overview/RunsOverviewModel.js | 3 ++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js index 19f6fb02b4..5633751036 100644 --- a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -12,7 +12,6 @@ */ -import { beamModesProvider } from '../../../services/beamModes/beamModesProvider.js' import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; import { FilterModel } from '../common/FilterModel.js'; @@ -22,25 +21,27 @@ import { FilterModel } from '../common/FilterModel.js'; export class BeamModeFilterModel extends FilterModel { /** * Constructor + * + * @param {ObservableData>} beamModes$ observable remote data of objects representing beam modes */ - constructor() { - super({}); - - this._dropDownModel = new ObservableBasedSelectionDropdownModel(beamModesProvider.items$, ({ name }) => ({ value: name })); + constructor(beamModes$) { + super(); + this._selectionDropDownModel = new ObservableBasedSelectionDropdownModel(beamModes$, ({ name }) => ({ value: name })); + this._addSubmodel(this._selectionDropDownModel); } /** * @inheritDoc */ reset() { - this._dropDownModel.reset(); + this._selectionDropDownModel.reset(); } /** * @inheritDoc */ get isEmpty() { - return this._dropDownModel.isEmpty; + return this._selectionDropDownModel.isEmpty; } /** @@ -49,6 +50,13 @@ export class BeamModeFilterModel extends FilterModel { * @return {ObservableDropDownModel} the underlying dropdown model */ get dropDownModel() { - return this._dropDownModel; + return this._selectionDropDownModel; + } + + /** + * @inheritDoc + */ + get normalized() { + return this._selectionDropDownModel.selected; } } diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index cfad9dc912..39efaee592 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -35,6 +35,7 @@ import { SelectionFilterModel } from '../../../components/Filters/common/filters import { DataExportModel } from '../../../models/DataExportModel.js'; import { runsActiveColumns as dataExportConfiguration } from '../ActiveColumns/runsActiveColumns.js'; import { BeamModeFilterModel } from '../../../components/Filters/RunsFilter/BeamModeFilterModel.js'; +import { beamModesProvider } from '../../../services/beamModes/beamModesProvider.js' /** * Model representing handlers for runs page @@ -68,7 +69,7 @@ export class RunsOverviewModel extends OverviewPageModel { runDuration: new NumericalComparisonFilterModel({ scale: 60 * 1000 }), environmentIds: new RawTextFilterModel(), runTypes: new RunTypesFilterModel(runTypesProvider.items$), - beamModes: new BeamModeFilterModel(), + beamModes: new BeamModeFilterModel(beamModesProvider.items$), runQualities: new SelectionFilterModel({ availableOptions: RUN_QUALITIES.map((quality) => ({ label: quality.toUpperCase(), From 61f69ef8d8ccf7fa49562a68f671091ad3f23897 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Fri, 13 Feb 2026 11:27:20 +0100 Subject: [PATCH 14/23] generalise formatRunType into formatNamedValue --- .../Filters/RunsFilter/BeamModeFilterModel.js | 2 +- .../{formatRunType.js => formatNamedValue.js} | 11 +++----- .../Runs/ActiveColumns/runsActiveColumns.js | 25 +++++++------------ .../views/Runs/Details/runDetailsComponent.js | 4 +-- 4 files changed, 16 insertions(+), 26 deletions(-) rename lib/public/utilities/formatting/{formatRunType.js => formatNamedValue.js} (68%) diff --git a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js index 5633751036..8b9812fc4b 100644 --- a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -49,7 +49,7 @@ export class BeamModeFilterModel extends FilterModel { * * @return {ObservableDropDownModel} the underlying dropdown model */ - get dropDownModel() { + get selectionDropDownModel() { return this._selectionDropDownModel; } diff --git a/lib/public/utilities/formatting/formatRunType.js b/lib/public/utilities/formatting/formatNamedValue.js similarity index 68% rename from lib/public/utilities/formatting/formatRunType.js rename to lib/public/utilities/formatting/formatNamedValue.js index b478dd5e74..fef3fd9830 100644 --- a/lib/public/utilities/formatting/formatRunType.js +++ b/lib/public/utilities/formatting/formatNamedValue.js @@ -13,14 +13,11 @@ */ /** - * Fetches the run type and formats on the given state of the fetch - * @param {string|undefined|object} runType The run type can be an id, name or null + * Fetches the value and formats on the given state of the fetch + * @param {string|undefined|object} value The value can be an id, name or null * * @returns {string} The name or id or a '-' if null */ -export function formatRunType(runType) { - if (runType) { - return runType.name ? runType.name : runType; - } - return '-'; +export function formatNamedValue(value) { + return value?.name ?? value ?? '-' } diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 9beaf89503..03a136791e 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -20,7 +20,7 @@ import epnFilter from '../../../components/Filters/RunsFilter/epn.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { displayRunDuration } from '../format/displayRunDuration.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; -import { formatRunType } from '../../../utilities/formatting/formatRunType.js'; +import { formatNamedValue } from '../../../utilities/formatting/formatNamedValue.js'; import { runDefinitionFilter } from '../../../components/Filters/RunsFilter/runDefinitionFilter.js'; import { profiles } from '../../../components/common/table/profiles.js'; import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; @@ -151,25 +151,18 @@ export const runsActiveColumns = { balloon: (tags) => tags && tags.length > 0, }, beamModes: { - name: 'Beam Modes', + name: 'Beam Mode', visible: true, - profiles: { - [profiles.none]: null, - lhcFill: null, - environment: null, - runsPerDataPass: { visible: false }, - }, - classes: 'w-5 f6', - exportFormat: (beamModes) => beamModes?.length ? beamModes.map(({ text }) => text).join('-') : '-', + classes: 'cell-l f6 w-wrapped', + format: formatNamedValue, /** - * Beam Mode filter component + * Beam Modes filter component * - * @param {RunsOverviewModel} runModel the runs overview model - * @return {Component} the filter component + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the beam modes filter component */ - filter: (runModel) => selectionDropdown(runModel.filteringModel.get('beamModes').dropDownModel, { selectorPrefix: 'beamMode' }), - balloon: (beamMode) => beamMode && beamMode.length > 0, + filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropDownModel, { selectorPrefix: 'beam-mode' }), }, fillNumber: { name: 'Fill No.', @@ -418,7 +411,7 @@ export const runsActiveColumns = { name: 'Run Type', visible: false, classes: 'cell-l f6 w-wrapped', - format: formatRunType, + format: formatNamedValue, /** * Run types filter component diff --git a/lib/public/views/Runs/Details/runDetailsComponent.js b/lib/public/views/Runs/Details/runDetailsComponent.js index 14524bf8de..ebb7ae4fe7 100644 --- a/lib/public/views/Runs/Details/runDetailsComponent.js +++ b/lib/public/views/Runs/Details/runDetailsComponent.js @@ -33,7 +33,7 @@ import { displayRunDuration } from '../format/displayRunDuration.js'; import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; import { formatRunQuality } from '../format/formatRunQuality.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; -import { formatRunType } from '../../../utilities/formatting/formatRunType.js'; +import { formatNamedValue } from '../../../utilities/formatting/formatNamedValue.js'; import { formatBoolean } from '../../../utilities/formatting/formatBoolean.js'; import { formatFileSize } from '../../../utilities/formatting/formatFileSize.js'; import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; @@ -383,7 +383,7 @@ export const runDetailsComponent = (runDetailsModel, router) => runDetailsModel. ]), h('.flex-column.items-center.flex-grow', [ h('strong', 'Run Type'), - formatRunType(run.runType), + formatNamedValue(run.runType), ]), ]), ]), From 2243d24baafc42a947a7e291f5b2fedbf9f6ec37 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Fri, 13 Feb 2026 11:51:43 +0100 Subject: [PATCH 15/23] test: add integration test for new filter --- test/public/runs/overview.test.js | 1736 +++++++++++++++-------------- 1 file changed, 872 insertions(+), 864 deletions(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 5ceac005b2..676384b98d 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -79,283 +79,283 @@ module.exports = () => { expect(title).to.equal('AliceO2 Bookkeeping'); }); - describe('General page behaviour', () => { - before(() => goToPage(page, 'run-overview')); - beforeEach(async () => { - await navigateToRunsOverview(page); - await resetFilters(page); - }) - - it('shows correct datatypes in respective columns', async () => { - table = await page.$$('tr'); - firstRowId = await getFirstRow(table, page); - - // Expectations of header texts being of a certain datatype - const headerDatatypes = { - runNumber: (number) => typeof number == 'number', - fillNumber: (number) => typeof number == 'number', - timeO2Start: (date) => !isNaN(Date.parse(date)), - timeO2End: (date) => !isNaN(Date.parse(date)), - timeTrgStart: (date) => !isNaN(Date.parse(date)), - timeTrgEnd: (date) => !isNaN(Date.parse(date)), - runDuration: (fullDuration) => { - const [duration, unit] = fullDuration.split(' '); - return fullDuration === 'RUNNING' || 'UNKNOWN' || !isNaN(parseInt(duration, 10)) && unit === 'sec'; - }, - environmentId: (number) => typeof number == 'number', - runType: (string) => typeof string == 'string', - runQuality: (string) => RUN_QUALITIES.includes(string), - nDetectors: (number) => typeof number == 'number', - nFlps: (number) => typeof number == 'number', - nEpns: (string) => typeof string == 'string', - nSubtimeframes: (number) => typeof number == 'number', - bytesReadOut: (number) => typeof number == 'number', - dcs: (boolean) => typeof boolean == 'boolean', - epn: (boolean) => typeof boolean == 'boolean', - eorReasons: (string) => typeof string == 'string', - detectors: (string) => typeof string == 'string', - }; - - // We find the headers matching the datatype keys - const headers = await page.$$('th'); - const headerIndices = {}; - for (const [index, header] of headers.entries()) { - const headerContent = await page.evaluate((element) => element.id, header); - const matchingDatatype = Object.keys(headerDatatypes).find((key) => headerContent === key); - if (matchingDatatype !== undefined) { - headerIndices[index] = matchingDatatype; - } - } - - // We expect every value of a header matching a datatype key to actually be of that datatype - const firstRowCells = await page.$$(`#${firstRowId} td`); - for (const [index, cell] of firstRowCells.entries()) { - if (Object.keys(headerIndices).includes(index)) { - const cellContent = await page.evaluate((element) => element.innerText, cell); - const expectedDatatype = headerDatatypes[headerIndices[index]](cellContent); - expect(expectedDatatype).to.be.true; - } - } - }); - - it('Should display the correct items counter at the bottom of the page', async () => { - await navigateToRunsOverview(page); - - await expectInnerText(page, '#firstRowIndex', '1'); - await expectInnerText(page, '#lastRowIndex', '8'); - await expectInnerText(page, '#totalRowsCount', '109'); - }); - - it('successfully switch to raw timestamp display', async () => { - await expectInnerText(page, '#row106 td:nth-child(6)', '08/08/2019\n13:00:00'); - await expectInnerText(page, '#row106 td:nth-child(7)', '09/08/2019\n14:00:00'); - await pressElement(page, '#preferences-raw-timestamps', true); - - await expectInnerText(page, '#row106 td:nth-child(6)', '1565269200000'); - await expectInnerText(page, '#row106 td:nth-child(7)', '1565359200000'); - - // Go back to normal - await pressElement(page, '#preferences-raw-timestamps', true); - }); - - it('can switch to infinite mode in amountSelector', async () => { - const INFINITE_SCROLL_CHUNK = 19; - await navigateToRunsOverview(page); - - // Wait fot the table to be loaded, it should have at least 2 rows (not loading) but less than 19 rows (which is infinite scroll chunk) - await page.waitForSelector('table tbody tr:nth-child(2)'); - expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.be.null; - - const amountSelectorButtonSelector = await '#amountSelector button'; - - // Expect the dropdown options to be visible when it is selected - await pressElement(page, amountSelectorButtonSelector); - - const amountSelectorDropdown = await page.$('#amountSelector .dropup-menu'); - expect(Boolean(amountSelectorDropdown)).to.be.true; - - const infiniteModeButtonSelector = '#amountSelector .dropup-menu .menu-item:nth-last-child(-n +2)'; - await pressElement(page, infiniteModeButtonSelector); - - // Wait for the first chunk to be loaded - await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); - expect((await getInnerText(await page.$(amountSelectorButtonSelector))).trim().endsWith('Infinite')).to.be.true; - - await page.evaluate(() => { - document.querySelector('table tbody tr:last-child').scrollIntoView({ behavior: 'instant' }); - }); - - await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); - expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.not.be.null; - }); - - it('can set how many runs are available per page', async () => { - const amountSelectorId = '#amountSelector'; - const amountSelectorButtonSelector = `${amountSelectorId} button`; - await pressElement(page, amountSelectorButtonSelector); - - const amountSelectorDropdown = await page.$(`${amountSelectorId} .dropup-menu`); - expect(Boolean(amountSelectorDropdown)).to.be.true; - - const amountItems5 = `${amountSelectorId} .dropup-menu .menu-item:first-child`; - await pressElement(page, amountItems5); - - // Expect the amount selector to currently be set to 5 when the first option (5) is selected - await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); - await waitForTableLength(page, 5); - - // Expect the custom per page input to have red border and text color if wrong value typed - const customPerPageInput = await page.$(`${amountSelectorId} input[type=number]`); - await customPerPageInput.evaluate((input) => input.focus()); - await page.$eval(`${amountSelectorId} input[type=number]`, (el) => { - el.value = '1111'; - el.dispatchEvent(new Event('input')); - }); - await page.waitForSelector('input:invalid'); - }); - - it('dynamically switches between visible pages in the page selector', async () => { - // Override the amount of runs visible per page manually - await navigateToRunsOverview(page); - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.runs.overviewModel.pagination.itemsPerPage = 1; - }); - await waitForTableLength(page, 1); - - // Expect the page five button to now be visible, but no more than that - const pageFiveButton = await page.$('#page5'); - expect(Boolean(pageFiveButton)).to.be.true; - const pageSixButton = await page.$('#page6'); - expect(Boolean(pageSixButton)).to.be.false; - - // Expect the page one button to have fallen away when clicking on page five button - await pressElement(page, '#page5'); - await page.waitForSelector('#page1', { hidden: true }); - }); - - it('notifies if table loading returned an error', async () => { - await navigateToRunsOverview(page); - // eslint-disable-next-line no-return-assign, no-undef - await page.evaluate(() => model.runs.overviewModel.pagination.itemsPerPage = 200); - await page.waitForSelector('.alert-danger'); - - // We expect there to be a fitting error message - const expectedMessage = 'Invalid Attribute: "query.page.limit" must be less than or equal to 100'; - await expectInnerText(page, '.alert-danger', expectedMessage); - - // Revert changes for next test - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.runs.overviewModel.pagination.itemsPerPage = 10; - }); - await waitForTableLength(page, 10); - }); - - it('can navigate to a run detail page', async () => { - await navigateToRunsOverview(page); - - const firstLink = await page.waitForSelector('tbody tr:first-of-type a'); - const expectedRunNumber = await firstLink.evaluate((el) => el.innerText); - - await waitForNavigation(page, () => firstLink.evaluate((el) => el.click())); - - const redirectedUrl = await page.url(); - const urlParameters = redirectedUrl.slice(redirectedUrl.indexOf('?') + 1).split('&'); - - expect(urlParameters).to.contain('page=run-detail'); - expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); - }); - - it('Should have balloon on detector, tags and eor column', async () => { - // Run 106 has detectors and tags that overflow - await fillInput(page, filterPanelRunNumbersInputSelector, '106', ['change']); - await expectColumnValues(page, 'runNumber', ['106']); - - await checkColumnBalloon(page, 1, 2); - await checkColumnBalloon(page, 1, 3); - - // Run 1 has eor reasons that overflow - await fillInput(page, filterPanelRunNumbersInputSelector, '1,1', ['change']); - await expectColumnValues(page, 'runNumber', ['1']); - await checkColumnBalloon(page, 1, 16); - }); - - it('Should display balloon if the text overflows', async () => { - await checkColumnBalloon(page, 1, 2); - }); - - it('should successfully display duration without warning popover when run has trigger OFF', async () => { - const runDurationCell = await page.waitForSelector('#row107-runDuration'); - expect(await runDurationCell.$('.popover-trigger')).to.be.null; - expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); - }); - - it('should successfully display duration without warning popover when run has both trigger start and stop', async () => { - const runDurationCell = await page.waitForSelector('#row106-runDuration'); - expect(await runDurationCell.$('.popover-trigger')).to.be.null; - expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); - }); - - it('should successfully display UNKNOWN without warning popover when run last for more than 48 hours', async () => { - const runDurationCell = await page.waitForSelector('#row105-runDuration'); - expect(await runDurationCell.$('.popover-trigger')).to.be.null; - expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('UNKNOWN'); - }); - - it('should successfully display popover warning when run is missing trigger start', async () => { - const popoverContent = await getPopoverContent(await page.waitForSelector('#row104-runDuration .popover-trigger')); - expect(popoverContent).to.equal('Duration based on o2 start because of missing trigger start information'); - }); - - it('should successfully display popover warning when run is missing trigger stop', async () => { - const popoverContent = await getPopoverContent(await page.waitForSelector('#row103-runDuration .popover-trigger')); - expect(popoverContent).to.equal('Duration based on o2 stop because of missing trigger stop information'); - }); - - it('should successfully display popover warning when run is missing trigger start and stop', async () => { - const popoverContent = await getPopoverContent(await page.waitForSelector('#row102-runDuration .popover-trigger')); - expect(popoverContent).to.equal('Duration based on o2 start AND stop because of missing trigger information'); - }); - - it('should successfully navigate to the LHC fill details page', async () => { - await waitForNavigation(page, () => pressElement(page, '#row108-fillNumber a')); - expectUrlParams(page, { page: 'lhc-fill-details', fillNumber: 1 }); - }); - - it('should successfully display links to infologger, QC GUI and ECS', async () => { - const { id: createdRunId, runNumber: createdRunNumber } = await runService.create({ runNumber: 1000, timeTrgStart: new Date(), environmentId: 'CmCvjNbg' }); - await waitForNavigation(page, () => pressElement(page, 'a#home')); - await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); - - // Not running run, wait for popover to be visible - await pressElement(page, '#row104-runNumber-text .popover-trigger'); - let popoverSelector = await getPopoverSelector(await page.waitForSelector('#row104-runNumber-text .popover-trigger')); - await page.waitForSelector(popoverSelector); - - await expectLink(page, `${popoverSelector} a:nth-of-type(1)`, { - href: 'http://localhost:8081/?q={%22partition%22:{%22match%22:%22TDI59So3d%22},' - + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', - innerText: 'Infologger FLP', - }); - await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { - href: 'http://localhost:8082/' + - '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', - innerText: 'QCG', - }); - - // Running run - await pressElement(page, `#row${createdRunId}-runNumber-text .popover-trigger`); - popoverSelector = await getPopoverSelector(await page.waitForSelector('#row110-runNumber-text .popover-trigger')); - await page.waitForSelector(popoverSelector); - - await expectLink(page, `${popoverSelector} a:nth-of-type(3)`, { - href: 'http://localhost:8080/?page=environment&id=CmCvjNbg', - innerText: 'ECS', - }); - await RunRepository.removeOne({ where: { runNumber: createdRunNumber } }); - }); - }) + // describe('General page behaviour', () => { + // before(() => goToPage(page, 'run-overview')); + // beforeEach(async () => { + // await navigateToRunsOverview(page); + // await resetFilters(page); + // }) + + // it('shows correct datatypes in respective columns', async () => { + // table = await page.$$('tr'); + // firstRowId = await getFirstRow(table, page); + + // // Expectations of header texts being of a certain datatype + // const headerDatatypes = { + // runNumber: (number) => typeof number == 'number', + // fillNumber: (number) => typeof number == 'number', + // timeO2Start: (date) => !isNaN(Date.parse(date)), + // timeO2End: (date) => !isNaN(Date.parse(date)), + // timeTrgStart: (date) => !isNaN(Date.parse(date)), + // timeTrgEnd: (date) => !isNaN(Date.parse(date)), + // runDuration: (fullDuration) => { + // const [duration, unit] = fullDuration.split(' '); + // return fullDuration === 'RUNNING' || 'UNKNOWN' || !isNaN(parseInt(duration, 10)) && unit === 'sec'; + // }, + // environmentId: (number) => typeof number == 'number', + // runType: (string) => typeof string == 'string', + // runQuality: (string) => RUN_QUALITIES.includes(string), + // nDetectors: (number) => typeof number == 'number', + // nFlps: (number) => typeof number == 'number', + // nEpns: (string) => typeof string == 'string', + // nSubtimeframes: (number) => typeof number == 'number', + // bytesReadOut: (number) => typeof number == 'number', + // dcs: (boolean) => typeof boolean == 'boolean', + // epn: (boolean) => typeof boolean == 'boolean', + // eorReasons: (string) => typeof string == 'string', + // detectors: (string) => typeof string == 'string', + // }; + + // // We find the headers matching the datatype keys + // const headers = await page.$$('th'); + // const headerIndices = {}; + // for (const [index, header] of headers.entries()) { + // const headerContent = await page.evaluate((element) => element.id, header); + // const matchingDatatype = Object.keys(headerDatatypes).find((key) => headerContent === key); + // if (matchingDatatype !== undefined) { + // headerIndices[index] = matchingDatatype; + // } + // } + + // // We expect every value of a header matching a datatype key to actually be of that datatype + // const firstRowCells = await page.$$(`#${firstRowId} td`); + // for (const [index, cell] of firstRowCells.entries()) { + // if (Object.keys(headerIndices).includes(index)) { + // const cellContent = await page.evaluate((element) => element.innerText, cell); + // const expectedDatatype = headerDatatypes[headerIndices[index]](cellContent); + // expect(expectedDatatype).to.be.true; + // } + // } + // }); + + // it('Should display the correct items counter at the bottom of the page', async () => { + // await navigateToRunsOverview(page); + + // await expectInnerText(page, '#firstRowIndex', '1'); + // await expectInnerText(page, '#lastRowIndex', '8'); + // await expectInnerText(page, '#totalRowsCount', '109'); + // }); + + // it('successfully switch to raw timestamp display', async () => { + // await expectInnerText(page, '#row106 td:nth-child(6)', '08/08/2019\n13:00:00'); + // await expectInnerText(page, '#row106 td:nth-child(7)', '09/08/2019\n14:00:00'); + // await pressElement(page, '#preferences-raw-timestamps', true); + + // await expectInnerText(page, '#row106 td:nth-child(6)', '1565269200000'); + // await expectInnerText(page, '#row106 td:nth-child(7)', '1565359200000'); + + // // Go back to normal + // await pressElement(page, '#preferences-raw-timestamps', true); + // }); + + // it('can switch to infinite mode in amountSelector', async () => { + // const INFINITE_SCROLL_CHUNK = 19; + // await navigateToRunsOverview(page); + + // // Wait fot the table to be loaded, it should have at least 2 rows (not loading) but less than 19 rows (which is infinite scroll chunk) + // await page.waitForSelector('table tbody tr:nth-child(2)'); + // expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.be.null; + + // const amountSelectorButtonSelector = await '#amountSelector button'; + + // // Expect the dropdown options to be visible when it is selected + // await pressElement(page, amountSelectorButtonSelector); + + // const amountSelectorDropdown = await page.$('#amountSelector .dropup-menu'); + // expect(Boolean(amountSelectorDropdown)).to.be.true; + + // const infiniteModeButtonSelector = '#amountSelector .dropup-menu .menu-item:nth-last-child(-n +2)'; + // await pressElement(page, infiniteModeButtonSelector); + + // // Wait for the first chunk to be loaded + // await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); + // expect((await getInnerText(await page.$(amountSelectorButtonSelector))).trim().endsWith('Infinite')).to.be.true; + + // await page.evaluate(() => { + // document.querySelector('table tbody tr:last-child').scrollIntoView({ behavior: 'instant' }); + // }); + + // await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); + // expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.not.be.null; + // }); + + // it('can set how many runs are available per page', async () => { + // const amountSelectorId = '#amountSelector'; + // const amountSelectorButtonSelector = `${amountSelectorId} button`; + // await pressElement(page, amountSelectorButtonSelector); + + // const amountSelectorDropdown = await page.$(`${amountSelectorId} .dropup-menu`); + // expect(Boolean(amountSelectorDropdown)).to.be.true; + + // const amountItems5 = `${amountSelectorId} .dropup-menu .menu-item:first-child`; + // await pressElement(page, amountItems5); + + // // Expect the amount selector to currently be set to 5 when the first option (5) is selected + // await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); + // await waitForTableLength(page, 5); + + // // Expect the custom per page input to have red border and text color if wrong value typed + // const customPerPageInput = await page.$(`${amountSelectorId} input[type=number]`); + // await customPerPageInput.evaluate((input) => input.focus()); + // await page.$eval(`${amountSelectorId} input[type=number]`, (el) => { + // el.value = '1111'; + // el.dispatchEvent(new Event('input')); + // }); + // await page.waitForSelector('input:invalid'); + // }); + + // it('dynamically switches between visible pages in the page selector', async () => { + // // Override the amount of runs visible per page manually + // await navigateToRunsOverview(page); + // await page.evaluate(() => { + // // eslint-disable-next-line no-undef + // model.runs.overviewModel.pagination.itemsPerPage = 1; + // }); + // await waitForTableLength(page, 1); + + // // Expect the page five button to now be visible, but no more than that + // const pageFiveButton = await page.$('#page5'); + // expect(Boolean(pageFiveButton)).to.be.true; + // const pageSixButton = await page.$('#page6'); + // expect(Boolean(pageSixButton)).to.be.false; + + // // Expect the page one button to have fallen away when clicking on page five button + // await pressElement(page, '#page5'); + // await page.waitForSelector('#page1', { hidden: true }); + // }); + + // it('notifies if table loading returned an error', async () => { + // await navigateToRunsOverview(page); + // // eslint-disable-next-line no-return-assign, no-undef + // await page.evaluate(() => model.runs.overviewModel.pagination.itemsPerPage = 200); + // await page.waitForSelector('.alert-danger'); + + // // We expect there to be a fitting error message + // const expectedMessage = 'Invalid Attribute: "query.page.limit" must be less than or equal to 100'; + // await expectInnerText(page, '.alert-danger', expectedMessage); + + // // Revert changes for next test + // await page.evaluate(() => { + // // eslint-disable-next-line no-undef + // model.runs.overviewModel.pagination.itemsPerPage = 10; + // }); + // await waitForTableLength(page, 10); + // }); + + // it('can navigate to a run detail page', async () => { + // await navigateToRunsOverview(page); + + // const firstLink = await page.waitForSelector('tbody tr:first-of-type a'); + // const expectedRunNumber = await firstLink.evaluate((el) => el.innerText); + + // await waitForNavigation(page, () => firstLink.evaluate((el) => el.click())); + + // const redirectedUrl = await page.url(); + // const urlParameters = redirectedUrl.slice(redirectedUrl.indexOf('?') + 1).split('&'); + + // expect(urlParameters).to.contain('page=run-detail'); + // expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); + // }); + + // it('Should have balloon on detector, tags and eor column', async () => { + // // Run 106 has detectors and tags that overflow + // await fillInput(page, filterPanelRunNumbersInputSelector, '106', ['change']); + // await expectColumnValues(page, 'runNumber', ['106']); + + // await checkColumnBalloon(page, 1, 2); + // await checkColumnBalloon(page, 1, 3); + + // // Run 1 has eor reasons that overflow + // await fillInput(page, filterPanelRunNumbersInputSelector, '1,1', ['change']); + // await expectColumnValues(page, 'runNumber', ['1']); + // await checkColumnBalloon(page, 1, 16); + // }); + + // it('Should display balloon if the text overflows', async () => { + // await checkColumnBalloon(page, 1, 2); + // }); + + // it('should successfully display duration without warning popover when run has trigger OFF', async () => { + // const runDurationCell = await page.waitForSelector('#row107-runDuration'); + // expect(await runDurationCell.$('.popover-trigger')).to.be.null; + // expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); + // }); + + // it('should successfully display duration without warning popover when run has both trigger start and stop', async () => { + // const runDurationCell = await page.waitForSelector('#row106-runDuration'); + // expect(await runDurationCell.$('.popover-trigger')).to.be.null; + // expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); + // }); + + // it('should successfully display UNKNOWN without warning popover when run last for more than 48 hours', async () => { + // const runDurationCell = await page.waitForSelector('#row105-runDuration'); + // expect(await runDurationCell.$('.popover-trigger')).to.be.null; + // expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('UNKNOWN'); + // }); + + // it('should successfully display popover warning when run is missing trigger start', async () => { + // const popoverContent = await getPopoverContent(await page.waitForSelector('#row104-runDuration .popover-trigger')); + // expect(popoverContent).to.equal('Duration based on o2 start because of missing trigger start information'); + // }); + + // it('should successfully display popover warning when run is missing trigger stop', async () => { + // const popoverContent = await getPopoverContent(await page.waitForSelector('#row103-runDuration .popover-trigger')); + // expect(popoverContent).to.equal('Duration based on o2 stop because of missing trigger stop information'); + // }); + + // it('should successfully display popover warning when run is missing trigger start and stop', async () => { + // const popoverContent = await getPopoverContent(await page.waitForSelector('#row102-runDuration .popover-trigger')); + // expect(popoverContent).to.equal('Duration based on o2 start AND stop because of missing trigger information'); + // }); + + // it('should successfully navigate to the LHC fill details page', async () => { + // await waitForNavigation(page, () => pressElement(page, '#row108-fillNumber a')); + // expectUrlParams(page, { page: 'lhc-fill-details', fillNumber: 1 }); + // }); + + // it('should successfully display links to infologger, QC GUI and ECS', async () => { + // const { id: createdRunId, runNumber: createdRunNumber } = await runService.create({ runNumber: 1000, timeTrgStart: new Date(), environmentId: 'CmCvjNbg' }); + // await waitForNavigation(page, () => pressElement(page, 'a#home')); + // await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); + + // // Not running run, wait for popover to be visible + // await pressElement(page, '#row104-runNumber-text .popover-trigger'); + // let popoverSelector = await getPopoverSelector(await page.waitForSelector('#row104-runNumber-text .popover-trigger')); + // await page.waitForSelector(popoverSelector); + + // await expectLink(page, `${popoverSelector} a:nth-of-type(1)`, { + // href: 'http://localhost:8081/?q={%22partition%22:{%22match%22:%22TDI59So3d%22},' + // + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', + // innerText: 'Infologger FLP', + // }); + // await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { + // href: 'http://localhost:8082/' + + // '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', + // innerText: 'QCG', + // }); + + // // Running run + // await pressElement(page, `#row${createdRunId}-runNumber-text .popover-trigger`); + // popoverSelector = await getPopoverSelector(await page.waitForSelector('#row110-runNumber-text .popover-trigger')); + // await page.waitForSelector(popoverSelector); + + // await expectLink(page, `${popoverSelector} a:nth-of-type(3)`, { + // href: 'http://localhost:8080/?page=environment&id=CmCvjNbg', + // innerText: 'ECS', + // }); + // await RunRepository.removeOne({ where: { runNumber: createdRunNumber } }); + // }); + // }) describe("Filters", () => { @@ -367,599 +367,607 @@ module.exports = () => { await resetFilters(page); }) - it('should successfully filter on detectors', async () => { - await page.waitForSelector('.detectors-filter .dropdown-trigger'); - await pressElement(page, '.detectors-filter .dropdown-trigger', true); - await pressElement(page, '#detector-filter-dropdown-option-ITS', true); - await pressElement(page, '#detector-filter-dropdown-option-FT0', true); - await waitForTableLength(page, 4); - - await pressElement(page, '#detector-filter-combination-operator-radio-button-or', true); - await waitForTableLength(page, 8); - - await pressElement(page, '#detector-filter-combination-operator-radio-button-none', true); - await waitForTableLength(page, 2); - }); - - it('should successfully filter on tags', async () => { - // Open filter toggle and wait for the dropdown to be visible - await pressElement(page, '.tags-filter .dropdown-trigger', true); - await pressElement(page, '#tag-dropdown-option-FOOD', true); - await pressElement(page, '#tag-dropdown-option-RUN', true); - await waitForTableLength(page, 1); - - await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); - await pressElement(page, '.tags-filter .dropdown-trigger', true); - await pressElement(page, '#tag-dropdown-option-RUN', true); - await pressElement(page, '#tag-dropdown-option-TEST-TAG-41', true); - await waitForTableLength(page, 2); - - await pressElement(page, '#tag-filter-combination-operator-radio-button-none-of', true); - await waitForTableTotalRowsCountToEqual(page, 107); - }); - - it('should successfully filter on definition', async () => { - const filterInputSelectorPrefix = '#run-definition-checkbox-'; - const physicsFilterSelector = `${filterInputSelectorPrefix}PHYSICS`; - const cosmicsFilterSelector = `${filterInputSelectorPrefix}COSMICS`; - const technicalFilterSelector = `${filterInputSelectorPrefix}TECHNICAL`; - const syntheticFilterSelector = `${filterInputSelectorPrefix}SYNTHETIC`; - const calibrationFilterSelector = `${filterInputSelectorPrefix}CALIBRATION`; - const commissioningFilterSelector = `${filterInputSelectorPrefix}COMMISSIONING`; - - /** - * Checks that all the rows of the given table have a valid run definition - * - * @param {number} size the expected size of the table - * @param {string[]} authorizedRunDefinition the list of valid run qualities - * @return {void} - */ - const checkTableSizeAndDefinition = async (size, authorizedRunDefinition) => { - // Wait for the table to have the proper size - await waitForTableLength(page, size); - - const definitions = await page.$$eval( - '.column-definition div div div:first-child', - (rows) => rows.map((row) => row.innerText), - ); - - try { - expect(definitions.every((definition) => authorizedRunDefinition.includes(definition))).to.be.true; - } catch { - const runNumbers = await page.$$eval('tbody tr', (rows) => rows.map((row) => { - const rowId = row.id; - return document.querySelector(`#${rowId}-runNumber-text`).innerText; - })); - throw new Error(`Expect all run definitions ${definitions} to be one of ${authorizedRunDefinition}, for runs (${runNumbers})`); - } - }; - - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.runs.overviewModel.pagination.itemsPerPage = 20; - }); - - await pressElement(page, physicsFilterSelector, true); - await checkTableSizeAndDefinition(11, [RunDefinition.PHYSICS]); - - await pressElement(page, syntheticFilterSelector, true); - await checkTableSizeAndDefinition(13, [RunDefinition.PHYSICS, RunDefinition.SYNTHETIC]); - - await pressElement(page, physicsFilterSelector, true); - await checkTableSizeAndDefinition(2, [RunDefinition.SYNTHETIC]); - - await pressElement(page, cosmicsFilterSelector, true); - await checkTableSizeAndDefinition(4, [RunDefinition.SYNTHETIC, RunDefinition.COSMICS]); - - await pressElement(page, syntheticFilterSelector, true); - await checkTableSizeAndDefinition(2, [RunDefinition.COSMICS]); - - await pressElement(page, technicalFilterSelector, true); - await checkTableSizeAndDefinition(3, [RunDefinition.COSMICS, RunDefinition.TECHNICAL]); - - await pressElement(page, cosmicsFilterSelector, true); - await checkTableSizeAndDefinition(1, [RunDefinition.TECHNICAL]); - - await pressElement(page, calibrationFilterSelector, true); - await checkTableSizeAndDefinition(2, [RunDefinition.TECHNICAL, RunDefinition.CALIBRATION]); - - await pressElement(page, commissioningFilterSelector, true); - await checkTableSizeAndDefinition(20, [RunDefinition.COMMISSIONING]); - - await pressElement(page, commissioningFilterSelector, true); - await pressElement(page, physicsFilterSelector, true); - await pressElement(page, syntheticFilterSelector, true); - await pressElement(page, cosmicsFilterSelector, true); - - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.runs.overviewModel.pagination.itemsPerPage = 20; - }); - - await checkTableSizeAndDefinition( - 17, - [RunDefinition.COSMICS, RunDefinition.TECHNICAL, RunDefinition.PHYSICS, RunDefinition.SYNTHETIC, RunDefinition.CALIBRATION], - ); - }); - - it('Should correctly set the the min/max of time range picker inputs', async () => { - await pressElement(page, '.timeO2Start-filter .popover-trigger'); - - const o2StartPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const periodInputsSelectors = getPeriodInputsSelectors(o2StartPopoverSelector); - - await fillInput(page, periodInputsSelectors.fromTimeSelector, '11:11', ['change']); - await fillInput(page, periodInputsSelectors.toTimeSelector, '14:00', ['change']); - - // American style input - await fillInput(page, periodInputsSelectors.fromDateSelector, '2021-02-03', ['change']); - await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-03', ['change']); - - // Wait for page to be refreshed - await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', '11:12'); - await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); - - await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', '13:59'); - await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-03'); - - // Setting different dates, still american style input - await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-05', ['change']); - - await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', ''); - await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); - - await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', ''); - await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-05'); - }); - - it('should successfully filter on duration', async () => { - await page.waitForSelector('#duration-operator'); - const runDurationOperatorSelector = '#duration-operator'; - const runDurationOperator = await page.$(runDurationOperatorSelector) || null; - expect(runDurationOperator).to.not.be.null; - expect(await runDurationOperator.evaluate((element) => element.value)).to.equal('='); - - const runDurationLimitSelector = '#duration-operand'; - await fillInput(page, runDurationLimitSelector, '1500', ['change']); - await waitForTableLength(page, 4); - - await page.select(runDurationOperatorSelector, '='); - await waitForTableLength(page, 4); - - let runDurationList = await getColumnCellsInnerTexts(page, 'runDuration'); - - expect(runDurationList.every((runDuration) => runDuration === '25:00:00')).to.be.true; - - await fillInput(page, runDurationLimitSelector, '3000', ['change']); - await waitForTableLength(page, 0); - - await page.select(runDurationOperatorSelector, '>='); - await waitForTableLength(page, 3); - - // Expect only unknown - runDurationList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { - const rowId = row.id; - return document.querySelector(`#${rowId}-runDuration-text`)?.innerText; - })); - - expect(runDurationList.every((runDuration) => runDuration === 'UNKNOWN')).to.be.true; - }); - - it('should successfully apply alice currents filters', async () => { - const popoverSelector = await getPopoverSelector(await page.waitForSelector('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - await pressElement(page, `${popoverSelector} .dropdown-option:last-child`, true); // Select 30003kA/0kA - - await expectColumnValues(page, 'runNumber', ['54', '53', '52']); - }); - - it('Should successfully filter runs by their run quality', async () => { - const filterInputSelectorPrefix = '#checkboxes-checkbox-'; - const badFilterSelector = `${filterInputSelectorPrefix}bad`; - const testFilterSelector = `${filterInputSelectorPrefix}test`; - - /** - * Checks that all the rows of the given table have a valid run quality - * - * @param {{evaluate: function}[]} rows the list of rows - * @param {string[]} authorizedRunQualities the list of valid run qualities - * @return {void} - */ - const checkTableRunQualities = async (rows, authorizedRunQualities) => { - for (const row of rows) { - expect(await row.evaluate((rowItem) => { - const rowId = rowItem.id; - return document.querySelector(`#${rowId}-runQuality-text`).innerText; - })).to.be.oneOf(authorizedRunQualities); - } - }; - - // Open filter toggle - await openFilteringPanel(page);; - await page.waitForSelector(badFilterSelector); - - await page.$eval(badFilterSelector, (element) => element.click()); - await waitForTableLength(page, 2); - - table = await page.$$('tbody tr'); - expect(table.length).to.equal(2); - await checkTableRunQualities(table, [RunQualities.BAD]); - - await page.$eval(testFilterSelector, (element) => element.click()); - await waitForTableLength(page, 20); - - table = await page.$$('tbody tr'); - await checkTableRunQualities(table, [RunQualities.BAD, RunQualities.TEST]); - - await page.$eval(testFilterSelector, (element) => element.click()); - await waitForTableLength(page, 2); - - table = await page.$$('tbody tr'); - expect(table.length).to.equal(2); - await checkTableRunQualities(table, [RunQualities.BAD]); - }); - - it('Should successfully filter runs by their trigger value', async () => { - await navigateToRunsOverview(page); - const filterInputSelectorPrefix = '#triggerValueCheckbox'; - const offFilterSelector = `${filterInputSelectorPrefix}OFF`; - const ltuFilterSelector = `${filterInputSelectorPrefix}LTU`; - - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.runs.overviewModel.pagination.itemsPerPage = 10; - }); - await waitForTableLength(page, 10); - - /** - * Checks that all the rows of the given table have a valid trigger value - * - * @param {{evaluate: function}[]} rows the list of rows - * @param {string[]} authorizedRunQualities the list of valid run qualities - * @return {void} - */ - const checkTableTriggerValue = async (rows, authorizedRunQualities) => { - for (const row of rows) { - expect(await row.evaluate((rowItem) => { - const rowId = rowItem.id; - return document.querySelector(`#${rowId}-triggerValue-text`).innerText; - })).to.be.oneOf(authorizedRunQualities); - } - }; - - // Open filter toggle - await openFilteringPanel(page);; - await page.waitForSelector(offFilterSelector); - - await page.$eval(offFilterSelector, (element) => element.click()); - await waitForTableLength(page, 9); - - table = await page.$$('tbody tr'); - await checkTableTriggerValue(table, ['OFF']); - - await page.$eval(ltuFilterSelector, (element) => element.click()); - await waitForTableLength(page, 10); - - table = await page.$$('tbody tr'); - await checkTableTriggerValue(table, ['OFF', 'LTU']); - - await page.$eval(ltuFilterSelector, (element) => element.click()); - await waitForTableLength(page, 9); - - table = await page.$$('tbody tr'); - await checkTableTriggerValue(table, ['OFF']); - }); - - it('should successfully filter on a list of run numbers and inform the user about it', async () => { - const inputValue = '101, 102'; - await navigateToRunsOverview(page); - - /** - * This is the sequence to test filtering the runs on run numbers. - * - * @param {string} selector the filter input selector - * @return {void} - */ - const filterOnRun = async (selector) => { - await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); - await fillInput(page, selector, inputValue, ['change']); - await waitForTableLength(page, 2); - // Validate amount in the table - const table = await page.$$('tbody tr'); - expect(table.length).to.equal(2); - expect(await page.$$eval('tbody tr', (rows) => rows.map((row) => row.id))).to.eql(['row102', 'row101']); - }; - - // First filter validation on the main page. - await filterOnRun('#runOverviewFilter .run-numbers-filter'); - // Validate if the filter tab value is equal to the main page value. - await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); - await resetFilters(page); - - // Run the same test sequence on the filter tab. - await filterOnRun(filterPanelRunNumbersInputSelector); - }); - - it('should successfully filter on a single run number and inform the user about it', async () => { - const inputValue = '10'; - - /** - * This is the sequence to test filtering the runs on run numbers. - * - * @param {string} selector the filter input selector - * @return {void} - */ - const filterOnRun = async (selector) => { - await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); - await fillInput(page, selector, inputValue, ['change']); - await expectColumnValues(page, 'runNumber', ['109', '108', '107', '106', '105', '104', '103', '102', '101', '100']); + // it('should successfully filter on detectors', async () => { + // await page.waitForSelector('.detectors-filter .dropdown-trigger'); + // await pressElement(page, '.detectors-filter .dropdown-trigger', true); + // await pressElement(page, '#detector-filter-dropdown-option-ITS', true); + // await pressElement(page, '#detector-filter-dropdown-option-FT0', true); + // await waitForTableLength(page, 4); + + // await pressElement(page, '#detector-filter-combination-operator-radio-button-or', true); + // await waitForTableLength(page, 8); + + // await pressElement(page, '#detector-filter-combination-operator-radio-button-none', true); + // await waitForTableLength(page, 2); + // }); + + // it('should successfully filter on tags', async () => { + // // Open filter toggle and wait for the dropdown to be visible + // await pressElement(page, '.tags-filter .dropdown-trigger', true); + // await pressElement(page, '#tag-dropdown-option-FOOD', true); + // await pressElement(page, '#tag-dropdown-option-RUN', true); + // await waitForTableLength(page, 1); + + // await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); + // await pressElement(page, '.tags-filter .dropdown-trigger', true); + // await pressElement(page, '#tag-dropdown-option-RUN', true); + // await pressElement(page, '#tag-dropdown-option-TEST-TAG-41', true); + // await waitForTableLength(page, 2); + + // await pressElement(page, '#tag-filter-combination-operator-radio-button-none-of', true); + // await waitForTableTotalRowsCountToEqual(page, 107); + // }); + + // it('should successfully filter on definition', async () => { + // const filterInputSelectorPrefix = '#run-definition-checkbox-'; + // const physicsFilterSelector = `${filterInputSelectorPrefix}PHYSICS`; + // const cosmicsFilterSelector = `${filterInputSelectorPrefix}COSMICS`; + // const technicalFilterSelector = `${filterInputSelectorPrefix}TECHNICAL`; + // const syntheticFilterSelector = `${filterInputSelectorPrefix}SYNTHETIC`; + // const calibrationFilterSelector = `${filterInputSelectorPrefix}CALIBRATION`; + // const commissioningFilterSelector = `${filterInputSelectorPrefix}COMMISSIONING`; + + // /** + // * Checks that all the rows of the given table have a valid run definition + // * + // * @param {number} size the expected size of the table + // * @param {string[]} authorizedRunDefinition the list of valid run qualities + // * @return {void} + // */ + // const checkTableSizeAndDefinition = async (size, authorizedRunDefinition) => { + // // Wait for the table to have the proper size + // await waitForTableLength(page, size); + + // const definitions = await page.$$eval( + // '.column-definition div div div:first-child', + // (rows) => rows.map((row) => row.innerText), + // ); + + // try { + // expect(definitions.every((definition) => authorizedRunDefinition.includes(definition))).to.be.true; + // } catch { + // const runNumbers = await page.$$eval('tbody tr', (rows) => rows.map((row) => { + // const rowId = row.id; + // return document.querySelector(`#${rowId}-runNumber-text`).innerText; + // })); + // throw new Error(`Expect all run definitions ${definitions} to be one of ${authorizedRunDefinition}, for runs (${runNumbers})`); + // } + // }; + + // await page.evaluate(() => { + // // eslint-disable-next-line no-undef + // model.runs.overviewModel.pagination.itemsPerPage = 20; + // }); + + // await pressElement(page, physicsFilterSelector, true); + // await checkTableSizeAndDefinition(11, [RunDefinition.PHYSICS]); + + // await pressElement(page, syntheticFilterSelector, true); + // await checkTableSizeAndDefinition(13, [RunDefinition.PHYSICS, RunDefinition.SYNTHETIC]); + + // await pressElement(page, physicsFilterSelector, true); + // await checkTableSizeAndDefinition(2, [RunDefinition.SYNTHETIC]); + + // await pressElement(page, cosmicsFilterSelector, true); + // await checkTableSizeAndDefinition(4, [RunDefinition.SYNTHETIC, RunDefinition.COSMICS]); + + // await pressElement(page, syntheticFilterSelector, true); + // await checkTableSizeAndDefinition(2, [RunDefinition.COSMICS]); + + // await pressElement(page, technicalFilterSelector, true); + // await checkTableSizeAndDefinition(3, [RunDefinition.COSMICS, RunDefinition.TECHNICAL]); + + // await pressElement(page, cosmicsFilterSelector, true); + // await checkTableSizeAndDefinition(1, [RunDefinition.TECHNICAL]); + + // await pressElement(page, calibrationFilterSelector, true); + // await checkTableSizeAndDefinition(2, [RunDefinition.TECHNICAL, RunDefinition.CALIBRATION]); + + // await pressElement(page, commissioningFilterSelector, true); + // await checkTableSizeAndDefinition(20, [RunDefinition.COMMISSIONING]); + + // await pressElement(page, commissioningFilterSelector, true); + // await pressElement(page, physicsFilterSelector, true); + // await pressElement(page, syntheticFilterSelector, true); + // await pressElement(page, cosmicsFilterSelector, true); + + // await page.evaluate(() => { + // // eslint-disable-next-line no-undef + // model.runs.overviewModel.pagination.itemsPerPage = 20; + // }); + + // await checkTableSizeAndDefinition( + // 17, + // [RunDefinition.COSMICS, RunDefinition.TECHNICAL, RunDefinition.PHYSICS, RunDefinition.SYNTHETIC, RunDefinition.CALIBRATION], + // ); + // }); + + // it('Should correctly set the the min/max of time range picker inputs', async () => { + // await pressElement(page, '.timeO2Start-filter .popover-trigger'); + + // const o2StartPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + // const periodInputsSelectors = getPeriodInputsSelectors(o2StartPopoverSelector); + + // await fillInput(page, periodInputsSelectors.fromTimeSelector, '11:11', ['change']); + // await fillInput(page, periodInputsSelectors.toTimeSelector, '14:00', ['change']); + + // // American style input + // await fillInput(page, periodInputsSelectors.fromDateSelector, '2021-02-03', ['change']); + // await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-03', ['change']); + + // // Wait for page to be refreshed + // await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', '11:12'); + // await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); + + // await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', '13:59'); + // await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-03'); + + // // Setting different dates, still american style input + // await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-05', ['change']); + + // await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', ''); + // await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); + + // await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', ''); + // await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-05'); + // }); + + // it('should successfully filter on duration', async () => { + // await page.waitForSelector('#duration-operator'); + // const runDurationOperatorSelector = '#duration-operator'; + // const runDurationOperator = await page.$(runDurationOperatorSelector) || null; + // expect(runDurationOperator).to.not.be.null; + // expect(await runDurationOperator.evaluate((element) => element.value)).to.equal('='); + + // const runDurationLimitSelector = '#duration-operand'; + // await fillInput(page, runDurationLimitSelector, '1500', ['change']); + // await waitForTableLength(page, 4); + + // await page.select(runDurationOperatorSelector, '='); + // await waitForTableLength(page, 4); + + // let runDurationList = await getColumnCellsInnerTexts(page, 'runDuration'); + + // expect(runDurationList.every((runDuration) => runDuration === '25:00:00')).to.be.true; + + // await fillInput(page, runDurationLimitSelector, '3000', ['change']); + // await waitForTableLength(page, 0); + + // await page.select(runDurationOperatorSelector, '>='); + // await waitForTableLength(page, 3); + + // // Expect only unknown + // runDurationList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { + // const rowId = row.id; + // return document.querySelector(`#${rowId}-runDuration-text`)?.innerText; + // })); + + // expect(runDurationList.every((runDuration) => runDuration === 'UNKNOWN')).to.be.true; + // }); + + // it('should successfully apply alice currents filters', async () => { + // const popoverSelector = await getPopoverSelector(await page.waitForSelector('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + // await pressElement(page, `${popoverSelector} .dropdown-option:last-child`, true); // Select 30003kA/0kA + + // await expectColumnValues(page, 'runNumber', ['54', '53', '52']); + // }); + + // it('Should successfully filter runs by their run quality', async () => { + // const filterInputSelectorPrefix = '#checkboxes-checkbox-'; + // const badFilterSelector = `${filterInputSelectorPrefix}bad`; + // const testFilterSelector = `${filterInputSelectorPrefix}test`; + + // /** + // * Checks that all the rows of the given table have a valid run quality + // * + // * @param {{evaluate: function}[]} rows the list of rows + // * @param {string[]} authorizedRunQualities the list of valid run qualities + // * @return {void} + // */ + // const checkTableRunQualities = async (rows, authorizedRunQualities) => { + // for (const row of rows) { + // expect(await row.evaluate((rowItem) => { + // const rowId = rowItem.id; + // return document.querySelector(`#${rowId}-runQuality-text`).innerText; + // })).to.be.oneOf(authorizedRunQualities); + // } + // }; + + // // Open filter toggle + // await openFilteringPanel(page);; + // await page.waitForSelector(badFilterSelector); + + // await page.$eval(badFilterSelector, (element) => element.click()); + // await waitForTableLength(page, 2); + + // table = await page.$$('tbody tr'); + // expect(table.length).to.equal(2); + // await checkTableRunQualities(table, [RunQualities.BAD]); + + // await page.$eval(testFilterSelector, (element) => element.click()); + // await waitForTableLength(page, 20); + + // table = await page.$$('tbody tr'); + // await checkTableRunQualities(table, [RunQualities.BAD, RunQualities.TEST]); + + // await page.$eval(testFilterSelector, (element) => element.click()); + // await waitForTableLength(page, 2); + + // table = await page.$$('tbody tr'); + // expect(table.length).to.equal(2); + // await checkTableRunQualities(table, [RunQualities.BAD]); + // }); + + // it('Should successfully filter runs by their trigger value', async () => { + // await navigateToRunsOverview(page); + // const filterInputSelectorPrefix = '#triggerValueCheckbox'; + // const offFilterSelector = `${filterInputSelectorPrefix}OFF`; + // const ltuFilterSelector = `${filterInputSelectorPrefix}LTU`; + + // await page.evaluate(() => { + // // eslint-disable-next-line no-undef + // model.runs.overviewModel.pagination.itemsPerPage = 10; + // }); + // await waitForTableLength(page, 10); + + // /** + // * Checks that all the rows of the given table have a valid trigger value + // * + // * @param {{evaluate: function}[]} rows the list of rows + // * @param {string[]} authorizedRunQualities the list of valid run qualities + // * @return {void} + // */ + // const checkTableTriggerValue = async (rows, authorizedRunQualities) => { + // for (const row of rows) { + // expect(await row.evaluate((rowItem) => { + // const rowId = rowItem.id; + // return document.querySelector(`#${rowId}-triggerValue-text`).innerText; + // })).to.be.oneOf(authorizedRunQualities); + // } + // }; + + // // Open filter toggle + // await openFilteringPanel(page);; + // await page.waitForSelector(offFilterSelector); + + // await page.$eval(offFilterSelector, (element) => element.click()); + // await waitForTableLength(page, 9); + + // table = await page.$$('tbody tr'); + // await checkTableTriggerValue(table, ['OFF']); + + // await page.$eval(ltuFilterSelector, (element) => element.click()); + // await waitForTableLength(page, 10); + + // table = await page.$$('tbody tr'); + // await checkTableTriggerValue(table, ['OFF', 'LTU']); + + // await page.$eval(ltuFilterSelector, (element) => element.click()); + // await waitForTableLength(page, 9); + + // table = await page.$$('tbody tr'); + // await checkTableTriggerValue(table, ['OFF']); + // }); + + // it('should successfully filter on a list of run numbers and inform the user about it', async () => { + // const inputValue = '101, 102'; + // await navigateToRunsOverview(page); + + // /** + // * This is the sequence to test filtering the runs on run numbers. + // * + // * @param {string} selector the filter input selector + // * @return {void} + // */ + // const filterOnRun = async (selector) => { + // await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); + // await fillInput(page, selector, inputValue, ['change']); + // await waitForTableLength(page, 2); + // // Validate amount in the table + // const table = await page.$$('tbody tr'); + // expect(table.length).to.equal(2); + // expect(await page.$$eval('tbody tr', (rows) => rows.map((row) => row.id))).to.eql(['row102', 'row101']); + // }; + + // // First filter validation on the main page. + // await filterOnRun('#runOverviewFilter .run-numbers-filter'); + // // Validate if the filter tab value is equal to the main page value. + // await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); + // await resetFilters(page); + + // // Run the same test sequence on the filter tab. + // await filterOnRun(filterPanelRunNumbersInputSelector); + // }); + + // it('should successfully filter on a single run number and inform the user about it', async () => { + // const inputValue = '10'; + + // /** + // * This is the sequence to test filtering the runs on run numbers. + // * + // * @param {string} selector the filter input selector + // * @return {void} + // */ + // const filterOnRun = async (selector) => { + // await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); + // await fillInput(page, selector, inputValue, ['change']); + // await expectColumnValues(page, 'runNumber', ['109', '108', '107', '106', '105', '104', '103', '102', '101', '100']); - await pressElement(page, '#pageMoveRight', true); - await expectColumnValues(page, 'runNumber', ['10']); - }; - - await filterOnRun('#runOverviewFilter .run-numbers-filter'); - await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); - await resetFilters(page); - await filterOnRun(filterPanelRunNumbersInputSelector); - }); - - it('should successfully filter on a list of fill numbers and inform the user about it', async () => { - await page.evaluate(() => window.model.disableInputDebounce()); - const filterInputSelector = '.fill-numbers-filter'; - expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. 7966, 7954, 7948...'); - - await fillInput(page, filterInputSelector, '1, 3', ['change']); - await waitForTableLength(page, 6); - }); - - it('should successfully filter on a list of environment ids and inform the user about it', async () => { - const filterInputSelector = '.environment-ids-filter'; - expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. Dxi029djX, TDI59So3d...'); - - await fillInput(page, filterInputSelector, 'Dxi029djX, TDI59So3d', ['change']); - await waitForTableLength(page, 6); - }); - - it('should successfully filter on run types', async () => { - await pressElement(page, '.runType-filter .dropdown-trigger'); - await pressElement(page, '#run-types-dropdown-option-2', true); - await pressElement(page, '#run-types-dropdown-option-14', true); - await waitForTableLength(page, 5); - }); - - it('should successfully filter on nDetectors', async () => { - await expectInputValue(page, '#nDetectors-operator', '='); - - await page.select('#nDetectors-operator', '<='); - await fillInput(page, '#nDetectors-operand', '1', ['change']); + // await pressElement(page, '#pageMoveRight', true); + // await expectColumnValues(page, 'runNumber', ['10']); + // }; + + // await filterOnRun('#runOverviewFilter .run-numbers-filter'); + // await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); + // await resetFilters(page); + // await filterOnRun(filterPanelRunNumbersInputSelector); + // }); + + // it('should successfully filter on a list of fill numbers and inform the user about it', async () => { + // await page.evaluate(() => window.model.disableInputDebounce()); + // const filterInputSelector = '.fill-numbers-filter'; + // expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. 7966, 7954, 7948...'); + + // await fillInput(page, filterInputSelector, '1, 3', ['change']); + // await waitForTableLength(page, 6); + // }); + + // it('should successfully filter on a list of environment ids and inform the user about it', async () => { + // const filterInputSelector = '.environment-ids-filter'; + // expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. Dxi029djX, TDI59So3d...'); + + // await fillInput(page, filterInputSelector, 'Dxi029djX, TDI59So3d', ['change']); + // await waitForTableLength(page, 6); + // }); + + // it('should successfully filter on run types', async () => { + // await pressElement(page, '.runType-filter .dropdown-trigger'); + // await pressElement(page, '#run-types-dropdown-option-2', true); + // await pressElement(page, '#run-types-dropdown-option-14', true); + // await waitForTableLength(page, 5); + // }); + + it('should successfully filter on beam mode', async () => { + await pressElement(page, '.beamModes-filter .dropdown-trigger'); + await pressElement(page, '#beam-mode-dropdown-option-NO\\ BEAM', true); + await waitForTableLength(page, 1); + await pressElement(page, '#beam-mode-dropdown-option-STABLE\\ BEAMS', true); await waitForTableLength(page, 6); - - const nDetectorsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { - const rowId = row.id; - return document.querySelector(`#${rowId}-detectors .nDetectors-badge`)?.innerText; - })); - - // The nDetectors can be null if the detectors' field is null but the nDetectors is not, which can be added in tests data - expect(nDetectorsList.every((nDetectors) => parseInt(nDetectors, 10) <= 1 || nDetectors === null)).to.be.true; - }); - - it('should successfully filter on nFLPs', async () => { - await expectInputValue(page, '#nFlps-operator', '='); - - await page.select('#nFlps-operator', '<='); - await fillInput(page, '#nFlps-operand', '10', ['change']); - await waitForTableLength(page, 5); - - const nFlpsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { - const rowId = row.id; - return document.querySelector(`#${rowId}-nFlps-text`)?.innerText; - })); - expect(nFlpsList.every((nFlps) => parseInt(nFlps, 10) <= 10)).to.be.true; }); - it('should successfully filter on nEPNs', async () => { - await expectInputValue(page, '#nEpns-operator', '='); - - await page.select('#nEpns-operator', '<='); - await fillInput(page, '#nEpns-operand', '10', ['change']); - await waitForTableLength(page, 5); - - await expectColumnValues(page, 'nEpns', ['10', '10', 'OFF', 'OFF', '10']); - }); - - it('should successfully filter on EPN on/off', async () => { - await pressElement(page, '#epnFilterRadioOFF', true); - await waitForTableLength(page, 2); - }); - - it('should successfully filter by EOR Reason types', async () => { - // Expect the EOR filter to exist - await page.waitForSelector('#eorCategories'); - await page.waitForSelector('#eorTitles'); - - // Select the EOR reason category DETECTORS - const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); - await page.select('#eorCategories', 'DETECTORS'); - await waitForTableLength(page, 3, undefined, oldTable); - await page.waitForSelector('#eorTitles option'); - let detectorTitleElements = await page.$$('#eorTitles option'); - expect(detectorTitleElements).has.lengthOf(3); - - // The titles dropdown should have updated - const detectorTitles = await Promise.all(detectorTitleElements - .map(async (element) => (await element.getProperty('value')).jsonValue())); - expect(detectorTitles).deep.to.equal(['', 'CPV', 'TPC']); - - /* - * The correct number of runs should be displayed in the table. - * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS' - */ - let eorReasons = await page.$$('table td[id$="eorReasons"]'); - expect(eorReasons).has.lengthOf(3); - - let eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); - - let allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS')); - expect(allTextsContainDetectors).to.be.true; - - // Select the EOR reason title CPV - await page.select('#eorTitles', 'CPV'); - await waitForTableLength(page, 2); - - /* - * The correct number of runs should be displayed in the table. - * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS - CPV' - */ - eorReasons = await page.$$('table td[id$="eorReasons"]'); - expect(eorReasons).has.lengthOf(2); - - eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); - - allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS - CPV')); - expect(allTextsContainDetectors).to.be.true; - - // Reset filters. There should be a single blank option in the EOR titles dropdown - await resetFilters(page) - await waitForTableLength(page, 10); - detectorTitleElements = await page.$$('#eorTitles option'); - expect(detectorTitleElements).has.lengthOf(1); - - // There should be many items in the run details table - eorReasons = await page.$$('table td[id$="eorReasons"]'); - expect(eorReasons.length).to.be.greaterThan(3); - }); - - it('should correctly filter by EOR reason description', async () => { - // Expect there to be one result that contains a certain description - const descriptionInput = 'some'; - await fillInput(page, '#eorDescription', descriptionInput, ['change']); - await waitForTableLength(page, 2); - - let eorReasons = await page.$$('table td[id$="eorReasons"]'); - expect(eorReasons).has.lengthOf(2); - const eorReasonText = await (await eorReasons[0].getProperty('innerText')).jsonValue(); - expect(eorReasonText.toLowerCase()).to.include(descriptionInput); - - // Assuming this result had the category DETECTORS, when we select a different category it should disappear. - await page.select('#eorCategories', 'OTHER'); - await waitForEmptyTable(page); - eorReasons = await page.$$('table td[id$="eorReasons"]'); - expect(eorReasons).has.lengthOf(0); - - // When we reset the filters, the input field should be empty - await resetFilters(page); - await waitForTableLength(page, 10); - eorReasons = await page.$$('table td[id$="eorReasons"]'); - expect(eorReasons.length).to.be.greaterThan(1); - - await expectInputValue(page, '#eorDescription', ''); - }); + // it('should successfully filter on nDetectors', async () => { + // await expectInputValue(page, '#nDetectors-operator', '='); + + // await page.select('#nDetectors-operator', '<='); + // await fillInput(page, '#nDetectors-operand', '1', ['change']); + // await waitForTableLength(page, 6); + + // const nDetectorsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { + // const rowId = row.id; + // return document.querySelector(`#${rowId}-detectors .nDetectors-badge`)?.innerText; + // })); + + // // The nDetectors can be null if the detectors' field is null but the nDetectors is not, which can be added in tests data + // expect(nDetectorsList.every((nDetectors) => parseInt(nDetectors, 10) <= 1 || nDetectors === null)).to.be.true; + // }); + + // it('should successfully filter on nFLPs', async () => { + // await expectInputValue(page, '#nFlps-operator', '='); + + // await page.select('#nFlps-operator', '<='); + // await fillInput(page, '#nFlps-operand', '10', ['change']); + // await waitForTableLength(page, 5); + + // const nFlpsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { + // const rowId = row.id; + // return document.querySelector(`#${rowId}-nFlps-text`)?.innerText; + // })); + // expect(nFlpsList.every((nFlps) => parseInt(nFlps, 10) <= 10)).to.be.true; + // }); + + // it('should successfully filter on nEPNs', async () => { + // await expectInputValue(page, '#nEpns-operator', '='); + + // await page.select('#nEpns-operator', '<='); + // await fillInput(page, '#nEpns-operand', '10', ['change']); + // await waitForTableLength(page, 5); + + // await expectColumnValues(page, 'nEpns', ['10', '10', 'OFF', 'OFF', '10']); + // }); + + // it('should successfully filter on EPN on/off', async () => { + // await pressElement(page, '#epnFilterRadioOFF', true); + // await waitForTableLength(page, 2); + // }); + + // it('should successfully filter by EOR Reason types', async () => { + // // Expect the EOR filter to exist + // await page.waitForSelector('#eorCategories'); + // await page.waitForSelector('#eorTitles'); + + // // Select the EOR reason category DETECTORS + // const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); + // await page.select('#eorCategories', 'DETECTORS'); + // await waitForTableLength(page, 3, undefined, oldTable); + // await page.waitForSelector('#eorTitles option'); + // let detectorTitleElements = await page.$$('#eorTitles option'); + // expect(detectorTitleElements).has.lengthOf(3); + + // // The titles dropdown should have updated + // const detectorTitles = await Promise.all(detectorTitleElements + // .map(async (element) => (await element.getProperty('value')).jsonValue())); + // expect(detectorTitles).deep.to.equal(['', 'CPV', 'TPC']); + + // /* + // * The correct number of runs should be displayed in the table. + // * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS' + // */ + // let eorReasons = await page.$$('table td[id$="eorReasons"]'); + // expect(eorReasons).has.lengthOf(3); + + // let eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); + + // let allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS')); + // expect(allTextsContainDetectors).to.be.true; + + // // Select the EOR reason title CPV + // await page.select('#eorTitles', 'CPV'); + // await waitForTableLength(page, 2); + + // /* + // * The correct number of runs should be displayed in the table. + // * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS - CPV' + // */ + // eorReasons = await page.$$('table td[id$="eorReasons"]'); + // expect(eorReasons).has.lengthOf(2); + + // eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); + + // allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS - CPV')); + // expect(allTextsContainDetectors).to.be.true; + + // // Reset filters. There should be a single blank option in the EOR titles dropdown + // await resetFilters(page) + // await waitForTableLength(page, 10); + // detectorTitleElements = await page.$$('#eorTitles option'); + // expect(detectorTitleElements).has.lengthOf(1); + + // // There should be many items in the run details table + // eorReasons = await page.$$('table td[id$="eorReasons"]'); + // expect(eorReasons.length).to.be.greaterThan(3); + // }); + + // it('should correctly filter by EOR reason description', async () => { + // // Expect there to be one result that contains a certain description + // const descriptionInput = 'some'; + // await fillInput(page, '#eorDescription', descriptionInput, ['change']); + // await waitForTableLength(page, 2); + + // let eorReasons = await page.$$('table td[id$="eorReasons"]'); + // expect(eorReasons).has.lengthOf(2); + // const eorReasonText = await (await eorReasons[0].getProperty('innerText')).jsonValue(); + // expect(eorReasonText.toLowerCase()).to.include(descriptionInput); + + // // Assuming this result had the category DETECTORS, when we select a different category it should disappear. + // await page.select('#eorCategories', 'OTHER'); + // await waitForEmptyTable(page); + // eorReasons = await page.$$('table td[id$="eorReasons"]'); + // expect(eorReasons).has.lengthOf(0); + + // // When we reset the filters, the input field should be empty + // await resetFilters(page); + // await waitForTableLength(page, 10); + // eorReasons = await page.$$('table td[id$="eorReasons"]'); + // expect(eorReasons.length).to.be.greaterThan(1); + + // await expectInputValue(page, '#eorDescription', ''); + // }); }) - describe("Export", () => { - const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; - - before(() => goToPage(page, 'run-overview')); - - beforeEach(async () => { - await navigateToRunsOverview(page); - await resetFilters(page); - }) - - it('should successfully display runs export button', async () => { - await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); - const runsExportButton = await page.$(EXPORT_RUNS_TRIGGER_SELECTOR); - expect(runsExportButton).to.be.not.null; - }); - - it('should successfully display runs export modal on click on export button', async () => { - let exportModal = await page.$('#export-data-modal'); - expect(exportModal).to.be.null; - - await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await page.waitForSelector('#export-data-modal', { timeout: 5000 }); - exportModal = await page.$('#export-data-modal'); - - expect(exportModal).to.not.be.null; - }); - - it('should successfully display information when export will be truncated', async () => { - await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true); - - const truncatedExportWarning = await page.waitForSelector('#export-data-modal #truncated-export-warning'); - expect(await truncatedExportWarning.evaluate((warning) => warning.innerText)) - .to - .equal('The data export is limited to 100 entries, only the most recent data will be exported'); - }); - - it('should successfully display disabled runs export button when there is no runs available', async () => { - await openFilteringPanel(page);; - - // Type a fake run number to have no runs - await fillInput(page, filterPanelRunNumbersInputSelector, '99999999999', ['change']); - await openFilteringPanel(page);; - - await page.waitForSelector(`${EXPORT_RUNS_TRIGGER_SELECTOR}:disabled`); - }); - - it('should successfully export filtered runs', async () => { - const targetFileName = 'data.json'; - - // First export - await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true) - await page.waitForSelector('#export-data-modal', { timeout: 5000 }); - await page.waitForSelector('#send:disabled'); - await page.waitForSelector('#export-data-modal select.form-control'); - await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); - await page.waitForSelector('#send:enabled'); - const exportButtonText = await page.$eval('#send', (button) => button.innerText); - expect(exportButtonText).to.be.eql('Export'); - - { - const downloadPath = await waitForDownload(page, () => pressElement(page, '#send', true)); - - // Check download - const downloadFilesNames = fs.readdirSync(downloadPath); - expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); - const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); - - expect(runs).to.be.lengthOf(100); - expect(runs.every(({ runQuality, runNumber, ...otherProps }) => - runQuality && runNumber && Object.keys(otherProps).length === 0)).to.be.true; - fs.unlinkSync(path.resolve(downloadPath, targetFileName)); - } - - // Second export - - // Apply filtering - await openFilteringPanel(page); + // describe("Export", () => { + // const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; + + // before(() => goToPage(page, 'run-overview')); + + // beforeEach(async () => { + // await navigateToRunsOverview(page); + // await resetFilters(page); + // }) + + // it('should successfully display runs export button', async () => { + // await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); + // const runsExportButton = await page.$(EXPORT_RUNS_TRIGGER_SELECTOR); + // expect(runsExportButton).to.be.not.null; + // }); + + // it('should successfully display runs export modal on click on export button', async () => { + // let exportModal = await page.$('#export-data-modal'); + // expect(exportModal).to.be.null; + + // await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); + // await page.waitForSelector('#export-data-modal', { timeout: 5000 }); + // exportModal = await page.$('#export-data-modal'); + + // expect(exportModal).to.not.be.null; + // }); + + // it('should successfully display information when export will be truncated', async () => { + // await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true); + + // const truncatedExportWarning = await page.waitForSelector('#export-data-modal #truncated-export-warning'); + // expect(await truncatedExportWarning.evaluate((warning) => warning.innerText)) + // .to + // .equal('The data export is limited to 100 entries, only the most recent data will be exported'); + // }); + + // it('should successfully display disabled runs export button when there is no runs available', async () => { + // await openFilteringPanel(page);; + + // // Type a fake run number to have no runs + // await fillInput(page, filterPanelRunNumbersInputSelector, '99999999999', ['change']); + // await openFilteringPanel(page);; + + // await page.waitForSelector(`${EXPORT_RUNS_TRIGGER_SELECTOR}:disabled`); + // }); + + // it('should successfully export filtered runs', async () => { + // const targetFileName = 'data.json'; + + // // First export + // await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true) + // await page.waitForSelector('#export-data-modal', { timeout: 5000 }); + // await page.waitForSelector('#send:disabled'); + // await page.waitForSelector('#export-data-modal select.form-control'); + // await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); + // await page.waitForSelector('#send:enabled'); + // const exportButtonText = await page.$eval('#send', (button) => button.innerText); + // expect(exportButtonText).to.be.eql('Export'); + + // { + // const downloadPath = await waitForDownload(page, () => pressElement(page, '#send', true)); + + // // Check download + // const downloadFilesNames = fs.readdirSync(downloadPath); + // expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); + // const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); + + // expect(runs).to.be.lengthOf(100); + // expect(runs.every(({ runQuality, runNumber, ...otherProps }) => + // runQuality && runNumber && Object.keys(otherProps).length === 0)).to.be.true; + // fs.unlinkSync(path.resolve(downloadPath, targetFileName)); + // } + + // // Second export + + // // Apply filtering + // await openFilteringPanel(page); - const filterInputSelectorPrefix = '#checkboxes-checkbox-'; - const badFilterSelector = `${filterInputSelectorPrefix}bad`; - - await openFilteringPanel(page);; - await page.waitForSelector(badFilterSelector); - await page.$eval(badFilterSelector, (element) => element.click()); - await page.waitForSelector('tbody tr:nth-child(2)'); - await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); - - ///// Download - await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await page.waitForSelector('#export-data-modal', { timeout: 5000 }); - - await page.waitForSelector('#export-data-modal select.form-control', { timeout: 10000 }); - await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); - - { - const downloadPath = await waitForDownload(page, () => pressElement(page, '#send:enabled', true)); - - // Check download - const downloadFilesNames = fs.readdirSync(downloadPath); - expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); - const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); - expect(runs).to.have.all.deep.members([{ runNumber: 2, runQuality: 'bad' }, { runNumber: 1, runQuality: 'bad' }]); - } - }); - }) + // const filterInputSelectorPrefix = '#checkboxes-checkbox-'; + // const badFilterSelector = `${filterInputSelectorPrefix}bad`; + + // await openFilteringPanel(page);; + // await page.waitForSelector(badFilterSelector); + // await page.$eval(badFilterSelector, (element) => element.click()); + // await page.waitForSelector('tbody tr:nth-child(2)'); + // await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); + + // ///// Download + // await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); + // await page.waitForSelector('#export-data-modal', { timeout: 5000 }); + + // await page.waitForSelector('#export-data-modal select.form-control', { timeout: 10000 }); + // await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); + + // { + // const downloadPath = await waitForDownload(page, () => pressElement(page, '#send:enabled', true)); + + // // Check download + // const downloadFilesNames = fs.readdirSync(downloadPath); + // expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); + // const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); + // expect(runs).to.have.all.deep.members([{ runNumber: 2, runQuality: 'bad' }, { runNumber: 1, runQuality: 'bad' }]); + // } + // }); + // }) }; From d237f951a92b7824e97f757526a18193ac26e373 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Fri, 13 Feb 2026 12:27:23 +0100 Subject: [PATCH 16/23] rename selectionDropDownModel to selectionDropdownModel --- .../Filters/RunsFilter/BeamModeFilterModel.js | 14 +++++++------- .../components/runTypes/RunTypesFilterModel.js | 12 ++++++------ .../views/Runs/ActiveColumns/runsActiveColumns.js | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js index 8b9812fc4b..88911ca32a 100644 --- a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -26,22 +26,22 @@ export class BeamModeFilterModel extends FilterModel { */ constructor(beamModes$) { super(); - this._selectionDropDownModel = new ObservableBasedSelectionDropdownModel(beamModes$, ({ name }) => ({ value: name })); - this._addSubmodel(this._selectionDropDownModel); + this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(beamModes$, ({ name }) => ({ value: name })); + this._addSubmodel(this._selectionDropdownModel); } /** * @inheritDoc */ reset() { - this._selectionDropDownModel.reset(); + this._selectionDropdownModel.reset(); } /** * @inheritDoc */ get isEmpty() { - return this._selectionDropDownModel.isEmpty; + return this._selectionDropdownModel.isEmpty; } /** @@ -49,14 +49,14 @@ export class BeamModeFilterModel extends FilterModel { * * @return {ObservableDropDownModel} the underlying dropdown model */ - get selectionDropDownModel() { - return this._selectionDropDownModel; + get selectionDropdownModel() { + return this._selectionDropdownModel; } /** * @inheritDoc */ get normalized() { - return this._selectionDropDownModel.selected; + return this._selectionDropdownModel.selected; } } diff --git a/lib/public/components/runTypes/RunTypesFilterModel.js b/lib/public/components/runTypes/RunTypesFilterModel.js index 15a94c88a0..60a923cbc6 100644 --- a/lib/public/components/runTypes/RunTypesFilterModel.js +++ b/lib/public/components/runTypes/RunTypesFilterModel.js @@ -26,29 +26,29 @@ export class RunTypesFilterModel extends FilterModel { */ constructor(runTypes$) { super(); - this._selectionDropDownModel = new ObservableBasedSelectionDropdownModel(runTypes$, runTypeToOption); - this._addSubmodel(this._selectionDropDownModel); + this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(runTypes$, runTypeToOption); + this._addSubmodel(this._selectionDropdownModel); } /** * @inheritDoc */ reset() { - this._selectionDropDownModel.reset(); + this._selectionDropdownModel.reset(); } /** * @inheritDoc */ get isEmpty() { - return this._selectionDropDownModel.isEmpty; + return this._selectionDropdownModel.isEmpty; } /** * @inheritDoc */ get normalized() { - return this._selectionDropDownModel.selected; + return this._selectionDropdownModel.selected; } /** @@ -57,6 +57,6 @@ export class RunTypesFilterModel extends FilterModel { * @return {SelectionDropdownModel} the selection dropdown model */ get selectionDropdownModel() { - return this._selectionDropDownModel; + return this._selectionDropdownModel; } } diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 03a136791e..b4b532847f 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -162,7 +162,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the beam modes filter component */ - filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropDownModel, { selectorPrefix: 'beam-mode' }), + filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropdownModel, { selectorPrefix: 'beam-mode' }), }, fillNumber: { name: 'Fill No.', From 9df8b6e1bbd541aa6d94e65077367504d0650885 Mon Sep 17 00:00:00 2001 From: NarrowsProjects <150556207+NarrowsProjects@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:57:13 +0100 Subject: [PATCH 17/23] [O2B-1498] Update api endpoint to filter runs by beam mode (#2078) - beamModes has been added to the getAllRunsDTO and is considered valid if: - Array entries consist of strings made up of uppercase words with single spaces between them --- lib/domain/dtos/filters/RunFilterDto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 93f3d5e238..c66a194778 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -34,7 +34,7 @@ const EorReasonFilterDto = Joi.object({ exports.RunFilterDto = Joi.object({ beamModes: CustomJoi.stringArray().items(Joi.string() .trim() - .pattern(/^[A-Z]+(?: [A-Z]+)*$/) + .pattern(/^[A-Z]{1,15}( [A-Z]{1,15}){0,2}/) .messages({ 'string.pattern.base': 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', From 9817746b26794a78a99e9ddc572695a4e5bd3cfe Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Fri, 13 Feb 2026 12:14:09 +0100 Subject: [PATCH 18/23] fix lint issues --- .../components/Filters/RunsFilter/BeamModeFilterModel.js | 3 +-- lib/public/utilities/formatting/formatNamedValue.js | 2 +- lib/public/views/Runs/ActiveColumns/runsActiveColumns.js | 3 ++- lib/public/views/Runs/Overview/RunsOverviewModel.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js index 88911ca32a..0704fc684d 100644 --- a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -11,7 +11,6 @@ * or submit itself to any jurisdiction. */ - import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; import { FilterModel } from '../common/FilterModel.js'; @@ -36,7 +35,7 @@ export class BeamModeFilterModel extends FilterModel { reset() { this._selectionDropdownModel.reset(); } - + /** * @inheritDoc */ diff --git a/lib/public/utilities/formatting/formatNamedValue.js b/lib/public/utilities/formatting/formatNamedValue.js index fef3fd9830..fc9750cfd0 100644 --- a/lib/public/utilities/formatting/formatNamedValue.js +++ b/lib/public/utilities/formatting/formatNamedValue.js @@ -19,5 +19,5 @@ * @returns {string} The name or id or a '-' if null */ export function formatNamedValue(value) { - return value?.name ?? value ?? '-' + return value?.name ?? value ?? '-'; } diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index b4b532847f..8448ff321f 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -162,7 +162,8 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the beam modes filter component */ - filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropdownModel, { selectorPrefix: 'beam-mode' }), + filter: (runsOverviewModel) => + selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropdownModel, { selectorPrefix: 'beam-mode' }), }, fillNumber: { name: 'Fill No.', diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 39efaee592..0249c66085 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -35,7 +35,7 @@ import { SelectionFilterModel } from '../../../components/Filters/common/filters import { DataExportModel } from '../../../models/DataExportModel.js'; import { runsActiveColumns as dataExportConfiguration } from '../ActiveColumns/runsActiveColumns.js'; import { BeamModeFilterModel } from '../../../components/Filters/RunsFilter/BeamModeFilterModel.js'; -import { beamModesProvider } from '../../../services/beamModes/beamModesProvider.js' +import { beamModesProvider } from '../../../services/beamModes/beamModesProvider.js'; /** * Model representing handlers for runs page From 0c6767a7f4f1c000c1cf5f7371d5a136378d2698 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Fri, 13 Feb 2026 12:46:35 +0100 Subject: [PATCH 19/23] chore: uncomment runs/overview tests --- test/public/runs/overview.test.js | 1732 ++++++++++++++--------------- 1 file changed, 866 insertions(+), 866 deletions(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 676384b98d..807b821ffc 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -79,283 +79,283 @@ module.exports = () => { expect(title).to.equal('AliceO2 Bookkeeping'); }); - // describe('General page behaviour', () => { - // before(() => goToPage(page, 'run-overview')); - // beforeEach(async () => { - // await navigateToRunsOverview(page); - // await resetFilters(page); - // }) - - // it('shows correct datatypes in respective columns', async () => { - // table = await page.$$('tr'); - // firstRowId = await getFirstRow(table, page); - - // // Expectations of header texts being of a certain datatype - // const headerDatatypes = { - // runNumber: (number) => typeof number == 'number', - // fillNumber: (number) => typeof number == 'number', - // timeO2Start: (date) => !isNaN(Date.parse(date)), - // timeO2End: (date) => !isNaN(Date.parse(date)), - // timeTrgStart: (date) => !isNaN(Date.parse(date)), - // timeTrgEnd: (date) => !isNaN(Date.parse(date)), - // runDuration: (fullDuration) => { - // const [duration, unit] = fullDuration.split(' '); - // return fullDuration === 'RUNNING' || 'UNKNOWN' || !isNaN(parseInt(duration, 10)) && unit === 'sec'; - // }, - // environmentId: (number) => typeof number == 'number', - // runType: (string) => typeof string == 'string', - // runQuality: (string) => RUN_QUALITIES.includes(string), - // nDetectors: (number) => typeof number == 'number', - // nFlps: (number) => typeof number == 'number', - // nEpns: (string) => typeof string == 'string', - // nSubtimeframes: (number) => typeof number == 'number', - // bytesReadOut: (number) => typeof number == 'number', - // dcs: (boolean) => typeof boolean == 'boolean', - // epn: (boolean) => typeof boolean == 'boolean', - // eorReasons: (string) => typeof string == 'string', - // detectors: (string) => typeof string == 'string', - // }; - - // // We find the headers matching the datatype keys - // const headers = await page.$$('th'); - // const headerIndices = {}; - // for (const [index, header] of headers.entries()) { - // const headerContent = await page.evaluate((element) => element.id, header); - // const matchingDatatype = Object.keys(headerDatatypes).find((key) => headerContent === key); - // if (matchingDatatype !== undefined) { - // headerIndices[index] = matchingDatatype; - // } - // } - - // // We expect every value of a header matching a datatype key to actually be of that datatype - // const firstRowCells = await page.$$(`#${firstRowId} td`); - // for (const [index, cell] of firstRowCells.entries()) { - // if (Object.keys(headerIndices).includes(index)) { - // const cellContent = await page.evaluate((element) => element.innerText, cell); - // const expectedDatatype = headerDatatypes[headerIndices[index]](cellContent); - // expect(expectedDatatype).to.be.true; - // } - // } - // }); - - // it('Should display the correct items counter at the bottom of the page', async () => { - // await navigateToRunsOverview(page); - - // await expectInnerText(page, '#firstRowIndex', '1'); - // await expectInnerText(page, '#lastRowIndex', '8'); - // await expectInnerText(page, '#totalRowsCount', '109'); - // }); - - // it('successfully switch to raw timestamp display', async () => { - // await expectInnerText(page, '#row106 td:nth-child(6)', '08/08/2019\n13:00:00'); - // await expectInnerText(page, '#row106 td:nth-child(7)', '09/08/2019\n14:00:00'); - // await pressElement(page, '#preferences-raw-timestamps', true); - - // await expectInnerText(page, '#row106 td:nth-child(6)', '1565269200000'); - // await expectInnerText(page, '#row106 td:nth-child(7)', '1565359200000'); - - // // Go back to normal - // await pressElement(page, '#preferences-raw-timestamps', true); - // }); - - // it('can switch to infinite mode in amountSelector', async () => { - // const INFINITE_SCROLL_CHUNK = 19; - // await navigateToRunsOverview(page); - - // // Wait fot the table to be loaded, it should have at least 2 rows (not loading) but less than 19 rows (which is infinite scroll chunk) - // await page.waitForSelector('table tbody tr:nth-child(2)'); - // expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.be.null; - - // const amountSelectorButtonSelector = await '#amountSelector button'; - - // // Expect the dropdown options to be visible when it is selected - // await pressElement(page, amountSelectorButtonSelector); - - // const amountSelectorDropdown = await page.$('#amountSelector .dropup-menu'); - // expect(Boolean(amountSelectorDropdown)).to.be.true; - - // const infiniteModeButtonSelector = '#amountSelector .dropup-menu .menu-item:nth-last-child(-n +2)'; - // await pressElement(page, infiniteModeButtonSelector); - - // // Wait for the first chunk to be loaded - // await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); - // expect((await getInnerText(await page.$(amountSelectorButtonSelector))).trim().endsWith('Infinite')).to.be.true; - - // await page.evaluate(() => { - // document.querySelector('table tbody tr:last-child').scrollIntoView({ behavior: 'instant' }); - // }); - - // await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); - // expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.not.be.null; - // }); - - // it('can set how many runs are available per page', async () => { - // const amountSelectorId = '#amountSelector'; - // const amountSelectorButtonSelector = `${amountSelectorId} button`; - // await pressElement(page, amountSelectorButtonSelector); - - // const amountSelectorDropdown = await page.$(`${amountSelectorId} .dropup-menu`); - // expect(Boolean(amountSelectorDropdown)).to.be.true; - - // const amountItems5 = `${amountSelectorId} .dropup-menu .menu-item:first-child`; - // await pressElement(page, amountItems5); - - // // Expect the amount selector to currently be set to 5 when the first option (5) is selected - // await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); - // await waitForTableLength(page, 5); - - // // Expect the custom per page input to have red border and text color if wrong value typed - // const customPerPageInput = await page.$(`${amountSelectorId} input[type=number]`); - // await customPerPageInput.evaluate((input) => input.focus()); - // await page.$eval(`${amountSelectorId} input[type=number]`, (el) => { - // el.value = '1111'; - // el.dispatchEvent(new Event('input')); - // }); - // await page.waitForSelector('input:invalid'); - // }); - - // it('dynamically switches between visible pages in the page selector', async () => { - // // Override the amount of runs visible per page manually - // await navigateToRunsOverview(page); - // await page.evaluate(() => { - // // eslint-disable-next-line no-undef - // model.runs.overviewModel.pagination.itemsPerPage = 1; - // }); - // await waitForTableLength(page, 1); - - // // Expect the page five button to now be visible, but no more than that - // const pageFiveButton = await page.$('#page5'); - // expect(Boolean(pageFiveButton)).to.be.true; - // const pageSixButton = await page.$('#page6'); - // expect(Boolean(pageSixButton)).to.be.false; - - // // Expect the page one button to have fallen away when clicking on page five button - // await pressElement(page, '#page5'); - // await page.waitForSelector('#page1', { hidden: true }); - // }); - - // it('notifies if table loading returned an error', async () => { - // await navigateToRunsOverview(page); - // // eslint-disable-next-line no-return-assign, no-undef - // await page.evaluate(() => model.runs.overviewModel.pagination.itemsPerPage = 200); - // await page.waitForSelector('.alert-danger'); - - // // We expect there to be a fitting error message - // const expectedMessage = 'Invalid Attribute: "query.page.limit" must be less than or equal to 100'; - // await expectInnerText(page, '.alert-danger', expectedMessage); - - // // Revert changes for next test - // await page.evaluate(() => { - // // eslint-disable-next-line no-undef - // model.runs.overviewModel.pagination.itemsPerPage = 10; - // }); - // await waitForTableLength(page, 10); - // }); - - // it('can navigate to a run detail page', async () => { - // await navigateToRunsOverview(page); - - // const firstLink = await page.waitForSelector('tbody tr:first-of-type a'); - // const expectedRunNumber = await firstLink.evaluate((el) => el.innerText); - - // await waitForNavigation(page, () => firstLink.evaluate((el) => el.click())); - - // const redirectedUrl = await page.url(); - // const urlParameters = redirectedUrl.slice(redirectedUrl.indexOf('?') + 1).split('&'); - - // expect(urlParameters).to.contain('page=run-detail'); - // expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); - // }); - - // it('Should have balloon on detector, tags and eor column', async () => { - // // Run 106 has detectors and tags that overflow - // await fillInput(page, filterPanelRunNumbersInputSelector, '106', ['change']); - // await expectColumnValues(page, 'runNumber', ['106']); - - // await checkColumnBalloon(page, 1, 2); - // await checkColumnBalloon(page, 1, 3); - - // // Run 1 has eor reasons that overflow - // await fillInput(page, filterPanelRunNumbersInputSelector, '1,1', ['change']); - // await expectColumnValues(page, 'runNumber', ['1']); - // await checkColumnBalloon(page, 1, 16); - // }); - - // it('Should display balloon if the text overflows', async () => { - // await checkColumnBalloon(page, 1, 2); - // }); - - // it('should successfully display duration without warning popover when run has trigger OFF', async () => { - // const runDurationCell = await page.waitForSelector('#row107-runDuration'); - // expect(await runDurationCell.$('.popover-trigger')).to.be.null; - // expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); - // }); - - // it('should successfully display duration without warning popover when run has both trigger start and stop', async () => { - // const runDurationCell = await page.waitForSelector('#row106-runDuration'); - // expect(await runDurationCell.$('.popover-trigger')).to.be.null; - // expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); - // }); - - // it('should successfully display UNKNOWN without warning popover when run last for more than 48 hours', async () => { - // const runDurationCell = await page.waitForSelector('#row105-runDuration'); - // expect(await runDurationCell.$('.popover-trigger')).to.be.null; - // expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('UNKNOWN'); - // }); - - // it('should successfully display popover warning when run is missing trigger start', async () => { - // const popoverContent = await getPopoverContent(await page.waitForSelector('#row104-runDuration .popover-trigger')); - // expect(popoverContent).to.equal('Duration based on o2 start because of missing trigger start information'); - // }); - - // it('should successfully display popover warning when run is missing trigger stop', async () => { - // const popoverContent = await getPopoverContent(await page.waitForSelector('#row103-runDuration .popover-trigger')); - // expect(popoverContent).to.equal('Duration based on o2 stop because of missing trigger stop information'); - // }); - - // it('should successfully display popover warning when run is missing trigger start and stop', async () => { - // const popoverContent = await getPopoverContent(await page.waitForSelector('#row102-runDuration .popover-trigger')); - // expect(popoverContent).to.equal('Duration based on o2 start AND stop because of missing trigger information'); - // }); - - // it('should successfully navigate to the LHC fill details page', async () => { - // await waitForNavigation(page, () => pressElement(page, '#row108-fillNumber a')); - // expectUrlParams(page, { page: 'lhc-fill-details', fillNumber: 1 }); - // }); - - // it('should successfully display links to infologger, QC GUI and ECS', async () => { - // const { id: createdRunId, runNumber: createdRunNumber } = await runService.create({ runNumber: 1000, timeTrgStart: new Date(), environmentId: 'CmCvjNbg' }); - // await waitForNavigation(page, () => pressElement(page, 'a#home')); - // await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); - - // // Not running run, wait for popover to be visible - // await pressElement(page, '#row104-runNumber-text .popover-trigger'); - // let popoverSelector = await getPopoverSelector(await page.waitForSelector('#row104-runNumber-text .popover-trigger')); - // await page.waitForSelector(popoverSelector); - - // await expectLink(page, `${popoverSelector} a:nth-of-type(1)`, { - // href: 'http://localhost:8081/?q={%22partition%22:{%22match%22:%22TDI59So3d%22},' - // + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', - // innerText: 'Infologger FLP', - // }); - // await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { - // href: 'http://localhost:8082/' + - // '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', - // innerText: 'QCG', - // }); - - // // Running run - // await pressElement(page, `#row${createdRunId}-runNumber-text .popover-trigger`); - // popoverSelector = await getPopoverSelector(await page.waitForSelector('#row110-runNumber-text .popover-trigger')); - // await page.waitForSelector(popoverSelector); - - // await expectLink(page, `${popoverSelector} a:nth-of-type(3)`, { - // href: 'http://localhost:8080/?page=environment&id=CmCvjNbg', - // innerText: 'ECS', - // }); - // await RunRepository.removeOne({ where: { runNumber: createdRunNumber } }); - // }); - // }) + describe('General page behaviour', () => { + before(() => goToPage(page, 'run-overview')); + beforeEach(async () => { + await navigateToRunsOverview(page); + await resetFilters(page); + }) + + it('shows correct datatypes in respective columns', async () => { + table = await page.$$('tr'); + firstRowId = await getFirstRow(table, page); + + // Expectations of header texts being of a certain datatype + const headerDatatypes = { + runNumber: (number) => typeof number == 'number', + fillNumber: (number) => typeof number == 'number', + timeO2Start: (date) => !isNaN(Date.parse(date)), + timeO2End: (date) => !isNaN(Date.parse(date)), + timeTrgStart: (date) => !isNaN(Date.parse(date)), + timeTrgEnd: (date) => !isNaN(Date.parse(date)), + runDuration: (fullDuration) => { + const [duration, unit] = fullDuration.split(' '); + return fullDuration === 'RUNNING' || 'UNKNOWN' || !isNaN(parseInt(duration, 10)) && unit === 'sec'; + }, + environmentId: (number) => typeof number == 'number', + runType: (string) => typeof string == 'string', + runQuality: (string) => RUN_QUALITIES.includes(string), + nDetectors: (number) => typeof number == 'number', + nFlps: (number) => typeof number == 'number', + nEpns: (string) => typeof string == 'string', + nSubtimeframes: (number) => typeof number == 'number', + bytesReadOut: (number) => typeof number == 'number', + dcs: (boolean) => typeof boolean == 'boolean', + epn: (boolean) => typeof boolean == 'boolean', + eorReasons: (string) => typeof string == 'string', + detectors: (string) => typeof string == 'string', + }; + + // We find the headers matching the datatype keys + const headers = await page.$$('th'); + const headerIndices = {}; + for (const [index, header] of headers.entries()) { + const headerContent = await page.evaluate((element) => element.id, header); + const matchingDatatype = Object.keys(headerDatatypes).find((key) => headerContent === key); + if (matchingDatatype !== undefined) { + headerIndices[index] = matchingDatatype; + } + } + + // We expect every value of a header matching a datatype key to actually be of that datatype + const firstRowCells = await page.$$(`#${firstRowId} td`); + for (const [index, cell] of firstRowCells.entries()) { + if (Object.keys(headerIndices).includes(index)) { + const cellContent = await page.evaluate((element) => element.innerText, cell); + const expectedDatatype = headerDatatypes[headerIndices[index]](cellContent); + expect(expectedDatatype).to.be.true; + } + } + }); + + it('Should display the correct items counter at the bottom of the page', async () => { + await navigateToRunsOverview(page); + + await expectInnerText(page, '#firstRowIndex', '1'); + await expectInnerText(page, '#lastRowIndex', '8'); + await expectInnerText(page, '#totalRowsCount', '109'); + }); + + it('successfully switch to raw timestamp display', async () => { + await expectInnerText(page, '#row106 td:nth-child(6)', '08/08/2019\n13:00:00'); + await expectInnerText(page, '#row106 td:nth-child(7)', '09/08/2019\n14:00:00'); + await pressElement(page, '#preferences-raw-timestamps', true); + + await expectInnerText(page, '#row106 td:nth-child(6)', '1565269200000'); + await expectInnerText(page, '#row106 td:nth-child(7)', '1565359200000'); + + // Go back to normal + await pressElement(page, '#preferences-raw-timestamps', true); + }); + + it('can switch to infinite mode in amountSelector', async () => { + const INFINITE_SCROLL_CHUNK = 19; + await navigateToRunsOverview(page); + + // Wait fot the table to be loaded, it should have at least 2 rows (not loading) but less than 19 rows (which is infinite scroll chunk) + await page.waitForSelector('table tbody tr:nth-child(2)'); + expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.be.null; + + const amountSelectorButtonSelector = await '#amountSelector button'; + + // Expect the dropdown options to be visible when it is selected + await pressElement(page, amountSelectorButtonSelector); + + const amountSelectorDropdown = await page.$('#amountSelector .dropup-menu'); + expect(Boolean(amountSelectorDropdown)).to.be.true; + + const infiniteModeButtonSelector = '#amountSelector .dropup-menu .menu-item:nth-last-child(-n +2)'; + await pressElement(page, infiniteModeButtonSelector); + + // Wait for the first chunk to be loaded + await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); + expect((await getInnerText(await page.$(amountSelectorButtonSelector))).trim().endsWith('Infinite')).to.be.true; + + await page.evaluate(() => { + document.querySelector('table tbody tr:last-child').scrollIntoView({ behavior: 'instant' }); + }); + + await page.waitForSelector(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`); + expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.not.be.null; + }); + + it('can set how many runs are available per page', async () => { + const amountSelectorId = '#amountSelector'; + const amountSelectorButtonSelector = `${amountSelectorId} button`; + await pressElement(page, amountSelectorButtonSelector); + + const amountSelectorDropdown = await page.$(`${amountSelectorId} .dropup-menu`); + expect(Boolean(amountSelectorDropdown)).to.be.true; + + const amountItems5 = `${amountSelectorId} .dropup-menu .menu-item:first-child`; + await pressElement(page, amountItems5); + + // Expect the amount selector to currently be set to 5 when the first option (5) is selected + await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); + await waitForTableLength(page, 5); + + // Expect the custom per page input to have red border and text color if wrong value typed + const customPerPageInput = await page.$(`${amountSelectorId} input[type=number]`); + await customPerPageInput.evaluate((input) => input.focus()); + await page.$eval(`${amountSelectorId} input[type=number]`, (el) => { + el.value = '1111'; + el.dispatchEvent(new Event('input')); + }); + await page.waitForSelector('input:invalid'); + }); + + it('dynamically switches between visible pages in the page selector', async () => { + // Override the amount of runs visible per page manually + await navigateToRunsOverview(page); + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.runs.overviewModel.pagination.itemsPerPage = 1; + }); + await waitForTableLength(page, 1); + + // Expect the page five button to now be visible, but no more than that + const pageFiveButton = await page.$('#page5'); + expect(Boolean(pageFiveButton)).to.be.true; + const pageSixButton = await page.$('#page6'); + expect(Boolean(pageSixButton)).to.be.false; + + // Expect the page one button to have fallen away when clicking on page five button + await pressElement(page, '#page5'); + await page.waitForSelector('#page1', { hidden: true }); + }); + + it('notifies if table loading returned an error', async () => { + await navigateToRunsOverview(page); + // eslint-disable-next-line no-return-assign, no-undef + await page.evaluate(() => model.runs.overviewModel.pagination.itemsPerPage = 200); + await page.waitForSelector('.alert-danger'); + + // We expect there to be a fitting error message + const expectedMessage = 'Invalid Attribute: "query.page.limit" must be less than or equal to 100'; + await expectInnerText(page, '.alert-danger', expectedMessage); + + // Revert changes for next test + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.runs.overviewModel.pagination.itemsPerPage = 10; + }); + await waitForTableLength(page, 10); + }); + + it('can navigate to a run detail page', async () => { + await navigateToRunsOverview(page); + + const firstLink = await page.waitForSelector('tbody tr:first-of-type a'); + const expectedRunNumber = await firstLink.evaluate((el) => el.innerText); + + await waitForNavigation(page, () => firstLink.evaluate((el) => el.click())); + + const redirectedUrl = await page.url(); + const urlParameters = redirectedUrl.slice(redirectedUrl.indexOf('?') + 1).split('&'); + + expect(urlParameters).to.contain('page=run-detail'); + expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); + }); + + it('Should have balloon on detector, tags and eor column', async () => { + // Run 106 has detectors and tags that overflow + await fillInput(page, filterPanelRunNumbersInputSelector, '106', ['change']); + await expectColumnValues(page, 'runNumber', ['106']); + + await checkColumnBalloon(page, 1, 2); + await checkColumnBalloon(page, 1, 3); + + // Run 1 has eor reasons that overflow + await fillInput(page, filterPanelRunNumbersInputSelector, '1,1', ['change']); + await expectColumnValues(page, 'runNumber', ['1']); + await checkColumnBalloon(page, 1, 16); + }); + + it('Should display balloon if the text overflows', async () => { + await checkColumnBalloon(page, 1, 2); + }); + + it('should successfully display duration without warning popover when run has trigger OFF', async () => { + const runDurationCell = await page.waitForSelector('#row107-runDuration'); + expect(await runDurationCell.$('.popover-trigger')).to.be.null; + expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); + }); + + it('should successfully display duration without warning popover when run has both trigger start and stop', async () => { + const runDurationCell = await page.waitForSelector('#row106-runDuration'); + expect(await runDurationCell.$('.popover-trigger')).to.be.null; + expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); + }); + + it('should successfully display UNKNOWN without warning popover when run last for more than 48 hours', async () => { + const runDurationCell = await page.waitForSelector('#row105-runDuration'); + expect(await runDurationCell.$('.popover-trigger')).to.be.null; + expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('UNKNOWN'); + }); + + it('should successfully display popover warning when run is missing trigger start', async () => { + const popoverContent = await getPopoverContent(await page.waitForSelector('#row104-runDuration .popover-trigger')); + expect(popoverContent).to.equal('Duration based on o2 start because of missing trigger start information'); + }); + + it('should successfully display popover warning when run is missing trigger stop', async () => { + const popoverContent = await getPopoverContent(await page.waitForSelector('#row103-runDuration .popover-trigger')); + expect(popoverContent).to.equal('Duration based on o2 stop because of missing trigger stop information'); + }); + + it('should successfully display popover warning when run is missing trigger start and stop', async () => { + const popoverContent = await getPopoverContent(await page.waitForSelector('#row102-runDuration .popover-trigger')); + expect(popoverContent).to.equal('Duration based on o2 start AND stop because of missing trigger information'); + }); + + it('should successfully navigate to the LHC fill details page', async () => { + await waitForNavigation(page, () => pressElement(page, '#row108-fillNumber a')); + expectUrlParams(page, { page: 'lhc-fill-details', fillNumber: 1 }); + }); + + it('should successfully display links to infologger, QC GUI and ECS', async () => { + const { id: createdRunId, runNumber: createdRunNumber } = await runService.create({ runNumber: 1000, timeTrgStart: new Date(), environmentId: 'CmCvjNbg' }); + await waitForNavigation(page, () => pressElement(page, 'a#home')); + await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); + + // Not running run, wait for popover to be visible + await pressElement(page, '#row104-runNumber-text .popover-trigger'); + let popoverSelector = await getPopoverSelector(await page.waitForSelector('#row104-runNumber-text .popover-trigger')); + await page.waitForSelector(popoverSelector); + + await expectLink(page, `${popoverSelector} a:nth-of-type(1)`, { + href: 'http://localhost:8081/?q={%22partition%22:{%22match%22:%22TDI59So3d%22},' + + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', + innerText: 'Infologger FLP', + }); + await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { + href: 'http://localhost:8082/' + + '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', + innerText: 'QCG', + }); + + // Running run + await pressElement(page, `#row${createdRunId}-runNumber-text .popover-trigger`); + popoverSelector = await getPopoverSelector(await page.waitForSelector('#row110-runNumber-text .popover-trigger')); + await page.waitForSelector(popoverSelector); + + await expectLink(page, `${popoverSelector} a:nth-of-type(3)`, { + href: 'http://localhost:8080/?page=environment&id=CmCvjNbg', + innerText: 'ECS', + }); + await RunRepository.removeOne({ where: { runNumber: createdRunNumber } }); + }); + }) describe("Filters", () => { @@ -367,365 +367,365 @@ module.exports = () => { await resetFilters(page); }) - // it('should successfully filter on detectors', async () => { - // await page.waitForSelector('.detectors-filter .dropdown-trigger'); - // await pressElement(page, '.detectors-filter .dropdown-trigger', true); - // await pressElement(page, '#detector-filter-dropdown-option-ITS', true); - // await pressElement(page, '#detector-filter-dropdown-option-FT0', true); - // await waitForTableLength(page, 4); - - // await pressElement(page, '#detector-filter-combination-operator-radio-button-or', true); - // await waitForTableLength(page, 8); - - // await pressElement(page, '#detector-filter-combination-operator-radio-button-none', true); - // await waitForTableLength(page, 2); - // }); - - // it('should successfully filter on tags', async () => { - // // Open filter toggle and wait for the dropdown to be visible - // await pressElement(page, '.tags-filter .dropdown-trigger', true); - // await pressElement(page, '#tag-dropdown-option-FOOD', true); - // await pressElement(page, '#tag-dropdown-option-RUN', true); - // await waitForTableLength(page, 1); - - // await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); - // await pressElement(page, '.tags-filter .dropdown-trigger', true); - // await pressElement(page, '#tag-dropdown-option-RUN', true); - // await pressElement(page, '#tag-dropdown-option-TEST-TAG-41', true); - // await waitForTableLength(page, 2); - - // await pressElement(page, '#tag-filter-combination-operator-radio-button-none-of', true); - // await waitForTableTotalRowsCountToEqual(page, 107); - // }); - - // it('should successfully filter on definition', async () => { - // const filterInputSelectorPrefix = '#run-definition-checkbox-'; - // const physicsFilterSelector = `${filterInputSelectorPrefix}PHYSICS`; - // const cosmicsFilterSelector = `${filterInputSelectorPrefix}COSMICS`; - // const technicalFilterSelector = `${filterInputSelectorPrefix}TECHNICAL`; - // const syntheticFilterSelector = `${filterInputSelectorPrefix}SYNTHETIC`; - // const calibrationFilterSelector = `${filterInputSelectorPrefix}CALIBRATION`; - // const commissioningFilterSelector = `${filterInputSelectorPrefix}COMMISSIONING`; - - // /** - // * Checks that all the rows of the given table have a valid run definition - // * - // * @param {number} size the expected size of the table - // * @param {string[]} authorizedRunDefinition the list of valid run qualities - // * @return {void} - // */ - // const checkTableSizeAndDefinition = async (size, authorizedRunDefinition) => { - // // Wait for the table to have the proper size - // await waitForTableLength(page, size); - - // const definitions = await page.$$eval( - // '.column-definition div div div:first-child', - // (rows) => rows.map((row) => row.innerText), - // ); - - // try { - // expect(definitions.every((definition) => authorizedRunDefinition.includes(definition))).to.be.true; - // } catch { - // const runNumbers = await page.$$eval('tbody tr', (rows) => rows.map((row) => { - // const rowId = row.id; - // return document.querySelector(`#${rowId}-runNumber-text`).innerText; - // })); - // throw new Error(`Expect all run definitions ${definitions} to be one of ${authorizedRunDefinition}, for runs (${runNumbers})`); - // } - // }; - - // await page.evaluate(() => { - // // eslint-disable-next-line no-undef - // model.runs.overviewModel.pagination.itemsPerPage = 20; - // }); - - // await pressElement(page, physicsFilterSelector, true); - // await checkTableSizeAndDefinition(11, [RunDefinition.PHYSICS]); - - // await pressElement(page, syntheticFilterSelector, true); - // await checkTableSizeAndDefinition(13, [RunDefinition.PHYSICS, RunDefinition.SYNTHETIC]); - - // await pressElement(page, physicsFilterSelector, true); - // await checkTableSizeAndDefinition(2, [RunDefinition.SYNTHETIC]); - - // await pressElement(page, cosmicsFilterSelector, true); - // await checkTableSizeAndDefinition(4, [RunDefinition.SYNTHETIC, RunDefinition.COSMICS]); - - // await pressElement(page, syntheticFilterSelector, true); - // await checkTableSizeAndDefinition(2, [RunDefinition.COSMICS]); - - // await pressElement(page, technicalFilterSelector, true); - // await checkTableSizeAndDefinition(3, [RunDefinition.COSMICS, RunDefinition.TECHNICAL]); - - // await pressElement(page, cosmicsFilterSelector, true); - // await checkTableSizeAndDefinition(1, [RunDefinition.TECHNICAL]); - - // await pressElement(page, calibrationFilterSelector, true); - // await checkTableSizeAndDefinition(2, [RunDefinition.TECHNICAL, RunDefinition.CALIBRATION]); - - // await pressElement(page, commissioningFilterSelector, true); - // await checkTableSizeAndDefinition(20, [RunDefinition.COMMISSIONING]); - - // await pressElement(page, commissioningFilterSelector, true); - // await pressElement(page, physicsFilterSelector, true); - // await pressElement(page, syntheticFilterSelector, true); - // await pressElement(page, cosmicsFilterSelector, true); - - // await page.evaluate(() => { - // // eslint-disable-next-line no-undef - // model.runs.overviewModel.pagination.itemsPerPage = 20; - // }); - - // await checkTableSizeAndDefinition( - // 17, - // [RunDefinition.COSMICS, RunDefinition.TECHNICAL, RunDefinition.PHYSICS, RunDefinition.SYNTHETIC, RunDefinition.CALIBRATION], - // ); - // }); - - // it('Should correctly set the the min/max of time range picker inputs', async () => { - // await pressElement(page, '.timeO2Start-filter .popover-trigger'); - - // const o2StartPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - // const periodInputsSelectors = getPeriodInputsSelectors(o2StartPopoverSelector); - - // await fillInput(page, periodInputsSelectors.fromTimeSelector, '11:11', ['change']); - // await fillInput(page, periodInputsSelectors.toTimeSelector, '14:00', ['change']); - - // // American style input - // await fillInput(page, periodInputsSelectors.fromDateSelector, '2021-02-03', ['change']); - // await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-03', ['change']); - - // // Wait for page to be refreshed - // await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', '11:12'); - // await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); - - // await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', '13:59'); - // await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-03'); - - // // Setting different dates, still american style input - // await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-05', ['change']); - - // await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', ''); - // await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); - - // await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', ''); - // await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-05'); - // }); - - // it('should successfully filter on duration', async () => { - // await page.waitForSelector('#duration-operator'); - // const runDurationOperatorSelector = '#duration-operator'; - // const runDurationOperator = await page.$(runDurationOperatorSelector) || null; - // expect(runDurationOperator).to.not.be.null; - // expect(await runDurationOperator.evaluate((element) => element.value)).to.equal('='); - - // const runDurationLimitSelector = '#duration-operand'; - // await fillInput(page, runDurationLimitSelector, '1500', ['change']); - // await waitForTableLength(page, 4); - - // await page.select(runDurationOperatorSelector, '='); - // await waitForTableLength(page, 4); - - // let runDurationList = await getColumnCellsInnerTexts(page, 'runDuration'); - - // expect(runDurationList.every((runDuration) => runDuration === '25:00:00')).to.be.true; - - // await fillInput(page, runDurationLimitSelector, '3000', ['change']); - // await waitForTableLength(page, 0); - - // await page.select(runDurationOperatorSelector, '>='); - // await waitForTableLength(page, 3); - - // // Expect only unknown - // runDurationList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { - // const rowId = row.id; - // return document.querySelector(`#${rowId}-runDuration-text`)?.innerText; - // })); - - // expect(runDurationList.every((runDuration) => runDuration === 'UNKNOWN')).to.be.true; - // }); - - // it('should successfully apply alice currents filters', async () => { - // const popoverSelector = await getPopoverSelector(await page.waitForSelector('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - // await pressElement(page, `${popoverSelector} .dropdown-option:last-child`, true); // Select 30003kA/0kA - - // await expectColumnValues(page, 'runNumber', ['54', '53', '52']); - // }); - - // it('Should successfully filter runs by their run quality', async () => { - // const filterInputSelectorPrefix = '#checkboxes-checkbox-'; - // const badFilterSelector = `${filterInputSelectorPrefix}bad`; - // const testFilterSelector = `${filterInputSelectorPrefix}test`; - - // /** - // * Checks that all the rows of the given table have a valid run quality - // * - // * @param {{evaluate: function}[]} rows the list of rows - // * @param {string[]} authorizedRunQualities the list of valid run qualities - // * @return {void} - // */ - // const checkTableRunQualities = async (rows, authorizedRunQualities) => { - // for (const row of rows) { - // expect(await row.evaluate((rowItem) => { - // const rowId = rowItem.id; - // return document.querySelector(`#${rowId}-runQuality-text`).innerText; - // })).to.be.oneOf(authorizedRunQualities); - // } - // }; - - // // Open filter toggle - // await openFilteringPanel(page);; - // await page.waitForSelector(badFilterSelector); - - // await page.$eval(badFilterSelector, (element) => element.click()); - // await waitForTableLength(page, 2); - - // table = await page.$$('tbody tr'); - // expect(table.length).to.equal(2); - // await checkTableRunQualities(table, [RunQualities.BAD]); - - // await page.$eval(testFilterSelector, (element) => element.click()); - // await waitForTableLength(page, 20); - - // table = await page.$$('tbody tr'); - // await checkTableRunQualities(table, [RunQualities.BAD, RunQualities.TEST]); - - // await page.$eval(testFilterSelector, (element) => element.click()); - // await waitForTableLength(page, 2); - - // table = await page.$$('tbody tr'); - // expect(table.length).to.equal(2); - // await checkTableRunQualities(table, [RunQualities.BAD]); - // }); - - // it('Should successfully filter runs by their trigger value', async () => { - // await navigateToRunsOverview(page); - // const filterInputSelectorPrefix = '#triggerValueCheckbox'; - // const offFilterSelector = `${filterInputSelectorPrefix}OFF`; - // const ltuFilterSelector = `${filterInputSelectorPrefix}LTU`; - - // await page.evaluate(() => { - // // eslint-disable-next-line no-undef - // model.runs.overviewModel.pagination.itemsPerPage = 10; - // }); - // await waitForTableLength(page, 10); - - // /** - // * Checks that all the rows of the given table have a valid trigger value - // * - // * @param {{evaluate: function}[]} rows the list of rows - // * @param {string[]} authorizedRunQualities the list of valid run qualities - // * @return {void} - // */ - // const checkTableTriggerValue = async (rows, authorizedRunQualities) => { - // for (const row of rows) { - // expect(await row.evaluate((rowItem) => { - // const rowId = rowItem.id; - // return document.querySelector(`#${rowId}-triggerValue-text`).innerText; - // })).to.be.oneOf(authorizedRunQualities); - // } - // }; - - // // Open filter toggle - // await openFilteringPanel(page);; - // await page.waitForSelector(offFilterSelector); - - // await page.$eval(offFilterSelector, (element) => element.click()); - // await waitForTableLength(page, 9); - - // table = await page.$$('tbody tr'); - // await checkTableTriggerValue(table, ['OFF']); - - // await page.$eval(ltuFilterSelector, (element) => element.click()); - // await waitForTableLength(page, 10); - - // table = await page.$$('tbody tr'); - // await checkTableTriggerValue(table, ['OFF', 'LTU']); - - // await page.$eval(ltuFilterSelector, (element) => element.click()); - // await waitForTableLength(page, 9); - - // table = await page.$$('tbody tr'); - // await checkTableTriggerValue(table, ['OFF']); - // }); - - // it('should successfully filter on a list of run numbers and inform the user about it', async () => { - // const inputValue = '101, 102'; - // await navigateToRunsOverview(page); - - // /** - // * This is the sequence to test filtering the runs on run numbers. - // * - // * @param {string} selector the filter input selector - // * @return {void} - // */ - // const filterOnRun = async (selector) => { - // await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); - // await fillInput(page, selector, inputValue, ['change']); - // await waitForTableLength(page, 2); - // // Validate amount in the table - // const table = await page.$$('tbody tr'); - // expect(table.length).to.equal(2); - // expect(await page.$$eval('tbody tr', (rows) => rows.map((row) => row.id))).to.eql(['row102', 'row101']); - // }; - - // // First filter validation on the main page. - // await filterOnRun('#runOverviewFilter .run-numbers-filter'); - // // Validate if the filter tab value is equal to the main page value. - // await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); - // await resetFilters(page); - - // // Run the same test sequence on the filter tab. - // await filterOnRun(filterPanelRunNumbersInputSelector); - // }); - - // it('should successfully filter on a single run number and inform the user about it', async () => { - // const inputValue = '10'; - - // /** - // * This is the sequence to test filtering the runs on run numbers. - // * - // * @param {string} selector the filter input selector - // * @return {void} - // */ - // const filterOnRun = async (selector) => { - // await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); - // await fillInput(page, selector, inputValue, ['change']); - // await expectColumnValues(page, 'runNumber', ['109', '108', '107', '106', '105', '104', '103', '102', '101', '100']); + it('should successfully filter on detectors', async () => { + await page.waitForSelector('.detectors-filter .dropdown-trigger'); + await pressElement(page, '.detectors-filter .dropdown-trigger', true); + await pressElement(page, '#detector-filter-dropdown-option-ITS', true); + await pressElement(page, '#detector-filter-dropdown-option-FT0', true); + await waitForTableLength(page, 4); + + await pressElement(page, '#detector-filter-combination-operator-radio-button-or', true); + await waitForTableLength(page, 8); + + await pressElement(page, '#detector-filter-combination-operator-radio-button-none', true); + await waitForTableLength(page, 2); + }); + + it('should successfully filter on tags', async () => { + // Open filter toggle and wait for the dropdown to be visible + await pressElement(page, '.tags-filter .dropdown-trigger', true); + await pressElement(page, '#tag-dropdown-option-FOOD', true); + await pressElement(page, '#tag-dropdown-option-RUN', true); + await waitForTableLength(page, 1); + + await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); + await pressElement(page, '.tags-filter .dropdown-trigger', true); + await pressElement(page, '#tag-dropdown-option-RUN', true); + await pressElement(page, '#tag-dropdown-option-TEST-TAG-41', true); + await waitForTableLength(page, 2); + + await pressElement(page, '#tag-filter-combination-operator-radio-button-none-of', true); + await waitForTableTotalRowsCountToEqual(page, 107); + }); + + it('should successfully filter on definition', async () => { + const filterInputSelectorPrefix = '#run-definition-checkbox-'; + const physicsFilterSelector = `${filterInputSelectorPrefix}PHYSICS`; + const cosmicsFilterSelector = `${filterInputSelectorPrefix}COSMICS`; + const technicalFilterSelector = `${filterInputSelectorPrefix}TECHNICAL`; + const syntheticFilterSelector = `${filterInputSelectorPrefix}SYNTHETIC`; + const calibrationFilterSelector = `${filterInputSelectorPrefix}CALIBRATION`; + const commissioningFilterSelector = `${filterInputSelectorPrefix}COMMISSIONING`; + + /** + * Checks that all the rows of the given table have a valid run definition + * + * @param {number} size the expected size of the table + * @param {string[]} authorizedRunDefinition the list of valid run qualities + * @return {void} + */ + const checkTableSizeAndDefinition = async (size, authorizedRunDefinition) => { + // Wait for the table to have the proper size + await waitForTableLength(page, size); + + const definitions = await page.$$eval( + '.column-definition div div div:first-child', + (rows) => rows.map((row) => row.innerText), + ); + + try { + expect(definitions.every((definition) => authorizedRunDefinition.includes(definition))).to.be.true; + } catch { + const runNumbers = await page.$$eval('tbody tr', (rows) => rows.map((row) => { + const rowId = row.id; + return document.querySelector(`#${rowId}-runNumber-text`).innerText; + })); + throw new Error(`Expect all run definitions ${definitions} to be one of ${authorizedRunDefinition}, for runs (${runNumbers})`); + } + }; + + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.runs.overviewModel.pagination.itemsPerPage = 20; + }); + + await pressElement(page, physicsFilterSelector, true); + await checkTableSizeAndDefinition(11, [RunDefinition.PHYSICS]); + + await pressElement(page, syntheticFilterSelector, true); + await checkTableSizeAndDefinition(13, [RunDefinition.PHYSICS, RunDefinition.SYNTHETIC]); + + await pressElement(page, physicsFilterSelector, true); + await checkTableSizeAndDefinition(2, [RunDefinition.SYNTHETIC]); + + await pressElement(page, cosmicsFilterSelector, true); + await checkTableSizeAndDefinition(4, [RunDefinition.SYNTHETIC, RunDefinition.COSMICS]); + + await pressElement(page, syntheticFilterSelector, true); + await checkTableSizeAndDefinition(2, [RunDefinition.COSMICS]); + + await pressElement(page, technicalFilterSelector, true); + await checkTableSizeAndDefinition(3, [RunDefinition.COSMICS, RunDefinition.TECHNICAL]); + + await pressElement(page, cosmicsFilterSelector, true); + await checkTableSizeAndDefinition(1, [RunDefinition.TECHNICAL]); + + await pressElement(page, calibrationFilterSelector, true); + await checkTableSizeAndDefinition(2, [RunDefinition.TECHNICAL, RunDefinition.CALIBRATION]); + + await pressElement(page, commissioningFilterSelector, true); + await checkTableSizeAndDefinition(20, [RunDefinition.COMMISSIONING]); + + await pressElement(page, commissioningFilterSelector, true); + await pressElement(page, physicsFilterSelector, true); + await pressElement(page, syntheticFilterSelector, true); + await pressElement(page, cosmicsFilterSelector, true); + + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.runs.overviewModel.pagination.itemsPerPage = 20; + }); + + await checkTableSizeAndDefinition( + 17, + [RunDefinition.COSMICS, RunDefinition.TECHNICAL, RunDefinition.PHYSICS, RunDefinition.SYNTHETIC, RunDefinition.CALIBRATION], + ); + }); + + it('Should correctly set the the min/max of time range picker inputs', async () => { + await pressElement(page, '.timeO2Start-filter .popover-trigger'); + + const o2StartPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const periodInputsSelectors = getPeriodInputsSelectors(o2StartPopoverSelector); + + await fillInput(page, periodInputsSelectors.fromTimeSelector, '11:11', ['change']); + await fillInput(page, periodInputsSelectors.toTimeSelector, '14:00', ['change']); + + // American style input + await fillInput(page, periodInputsSelectors.fromDateSelector, '2021-02-03', ['change']); + await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-03', ['change']); + + // Wait for page to be refreshed + await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', '11:12'); + await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); + + await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', '13:59'); + await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-03'); + + // Setting different dates, still american style input + await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-05', ['change']); + + await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', ''); + await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); + + await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', ''); + await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-05'); + }); + + it('should successfully filter on duration', async () => { + await page.waitForSelector('#duration-operator'); + const runDurationOperatorSelector = '#duration-operator'; + const runDurationOperator = await page.$(runDurationOperatorSelector) || null; + expect(runDurationOperator).to.not.be.null; + expect(await runDurationOperator.evaluate((element) => element.value)).to.equal('='); + + const runDurationLimitSelector = '#duration-operand'; + await fillInput(page, runDurationLimitSelector, '1500', ['change']); + await waitForTableLength(page, 4); + + await page.select(runDurationOperatorSelector, '='); + await waitForTableLength(page, 4); + + let runDurationList = await getColumnCellsInnerTexts(page, 'runDuration'); + + expect(runDurationList.every((runDuration) => runDuration === '25:00:00')).to.be.true; + + await fillInput(page, runDurationLimitSelector, '3000', ['change']); + await waitForTableLength(page, 0); + + await page.select(runDurationOperatorSelector, '>='); + await waitForTableLength(page, 3); + + // Expect only unknown + runDurationList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { + const rowId = row.id; + return document.querySelector(`#${rowId}-runDuration-text`)?.innerText; + })); + + expect(runDurationList.every((runDuration) => runDuration === 'UNKNOWN')).to.be.true; + }); + + it('should successfully apply alice currents filters', async () => { + const popoverSelector = await getPopoverSelector(await page.waitForSelector('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + await pressElement(page, `${popoverSelector} .dropdown-option:last-child`, true); // Select 30003kA/0kA + + await expectColumnValues(page, 'runNumber', ['54', '53', '52']); + }); + + it('Should successfully filter runs by their run quality', async () => { + const filterInputSelectorPrefix = '#checkboxes-checkbox-'; + const badFilterSelector = `${filterInputSelectorPrefix}bad`; + const testFilterSelector = `${filterInputSelectorPrefix}test`; + + /** + * Checks that all the rows of the given table have a valid run quality + * + * @param {{evaluate: function}[]} rows the list of rows + * @param {string[]} authorizedRunQualities the list of valid run qualities + * @return {void} + */ + const checkTableRunQualities = async (rows, authorizedRunQualities) => { + for (const row of rows) { + expect(await row.evaluate((rowItem) => { + const rowId = rowItem.id; + return document.querySelector(`#${rowId}-runQuality-text`).innerText; + })).to.be.oneOf(authorizedRunQualities); + } + }; + + // Open filter toggle + await openFilteringPanel(page);; + await page.waitForSelector(badFilterSelector); + + await page.$eval(badFilterSelector, (element) => element.click()); + await waitForTableLength(page, 2); + + table = await page.$$('tbody tr'); + expect(table.length).to.equal(2); + await checkTableRunQualities(table, [RunQualities.BAD]); + + await page.$eval(testFilterSelector, (element) => element.click()); + await waitForTableLength(page, 20); + + table = await page.$$('tbody tr'); + await checkTableRunQualities(table, [RunQualities.BAD, RunQualities.TEST]); + + await page.$eval(testFilterSelector, (element) => element.click()); + await waitForTableLength(page, 2); + + table = await page.$$('tbody tr'); + expect(table.length).to.equal(2); + await checkTableRunQualities(table, [RunQualities.BAD]); + }); + + it('Should successfully filter runs by their trigger value', async () => { + await navigateToRunsOverview(page); + const filterInputSelectorPrefix = '#triggerValueCheckbox'; + const offFilterSelector = `${filterInputSelectorPrefix}OFF`; + const ltuFilterSelector = `${filterInputSelectorPrefix}LTU`; + + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.runs.overviewModel.pagination.itemsPerPage = 10; + }); + await waitForTableLength(page, 10); + + /** + * Checks that all the rows of the given table have a valid trigger value + * + * @param {{evaluate: function}[]} rows the list of rows + * @param {string[]} authorizedRunQualities the list of valid run qualities + * @return {void} + */ + const checkTableTriggerValue = async (rows, authorizedRunQualities) => { + for (const row of rows) { + expect(await row.evaluate((rowItem) => { + const rowId = rowItem.id; + return document.querySelector(`#${rowId}-triggerValue-text`).innerText; + })).to.be.oneOf(authorizedRunQualities); + } + }; + + // Open filter toggle + await openFilteringPanel(page);; + await page.waitForSelector(offFilterSelector); + + await page.$eval(offFilterSelector, (element) => element.click()); + await waitForTableLength(page, 9); + + table = await page.$$('tbody tr'); + await checkTableTriggerValue(table, ['OFF']); + + await page.$eval(ltuFilterSelector, (element) => element.click()); + await waitForTableLength(page, 10); + + table = await page.$$('tbody tr'); + await checkTableTriggerValue(table, ['OFF', 'LTU']); + + await page.$eval(ltuFilterSelector, (element) => element.click()); + await waitForTableLength(page, 9); + + table = await page.$$('tbody tr'); + await checkTableTriggerValue(table, ['OFF']); + }); + + it('should successfully filter on a list of run numbers and inform the user about it', async () => { + const inputValue = '101, 102'; + await navigateToRunsOverview(page); + + /** + * This is the sequence to test filtering the runs on run numbers. + * + * @param {string} selector the filter input selector + * @return {void} + */ + const filterOnRun = async (selector) => { + await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); + await fillInput(page, selector, inputValue, ['change']); + await waitForTableLength(page, 2); + // Validate amount in the table + const table = await page.$$('tbody tr'); + expect(table.length).to.equal(2); + expect(await page.$$eval('tbody tr', (rows) => rows.map((row) => row.id))).to.eql(['row102', 'row101']); + }; + + // First filter validation on the main page. + await filterOnRun('#runOverviewFilter .run-numbers-filter'); + // Validate if the filter tab value is equal to the main page value. + await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); + await resetFilters(page); + + // Run the same test sequence on the filter tab. + await filterOnRun(filterPanelRunNumbersInputSelector); + }); + + it('should successfully filter on a single run number and inform the user about it', async () => { + const inputValue = '10'; + + /** + * This is the sequence to test filtering the runs on run numbers. + * + * @param {string} selector the filter input selector + * @return {void} + */ + const filterOnRun = async (selector) => { + await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); + await fillInput(page, selector, inputValue, ['change']); + await expectColumnValues(page, 'runNumber', ['109', '108', '107', '106', '105', '104', '103', '102', '101', '100']); - // await pressElement(page, '#pageMoveRight', true); - // await expectColumnValues(page, 'runNumber', ['10']); - // }; - - // await filterOnRun('#runOverviewFilter .run-numbers-filter'); - // await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); - // await resetFilters(page); - // await filterOnRun(filterPanelRunNumbersInputSelector); - // }); - - // it('should successfully filter on a list of fill numbers and inform the user about it', async () => { - // await page.evaluate(() => window.model.disableInputDebounce()); - // const filterInputSelector = '.fill-numbers-filter'; - // expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. 7966, 7954, 7948...'); - - // await fillInput(page, filterInputSelector, '1, 3', ['change']); - // await waitForTableLength(page, 6); - // }); - - // it('should successfully filter on a list of environment ids and inform the user about it', async () => { - // const filterInputSelector = '.environment-ids-filter'; - // expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. Dxi029djX, TDI59So3d...'); - - // await fillInput(page, filterInputSelector, 'Dxi029djX, TDI59So3d', ['change']); - // await waitForTableLength(page, 6); - // }); - - // it('should successfully filter on run types', async () => { - // await pressElement(page, '.runType-filter .dropdown-trigger'); - // await pressElement(page, '#run-types-dropdown-option-2', true); - // await pressElement(page, '#run-types-dropdown-option-14', true); - // await waitForTableLength(page, 5); - // }); + await pressElement(page, '#pageMoveRight', true); + await expectColumnValues(page, 'runNumber', ['10']); + }; + + await filterOnRun('#runOverviewFilter .run-numbers-filter'); + await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); + await resetFilters(page); + await filterOnRun(filterPanelRunNumbersInputSelector); + }); + + it('should successfully filter on a list of fill numbers and inform the user about it', async () => { + await page.evaluate(() => window.model.disableInputDebounce()); + const filterInputSelector = '.fill-numbers-filter'; + expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. 7966, 7954, 7948...'); + + await fillInput(page, filterInputSelector, '1, 3', ['change']); + await waitForTableLength(page, 6); + }); + + it('should successfully filter on a list of environment ids and inform the user about it', async () => { + const filterInputSelector = '.environment-ids-filter'; + expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. Dxi029djX, TDI59So3d...'); + + await fillInput(page, filterInputSelector, 'Dxi029djX, TDI59So3d', ['change']); + await waitForTableLength(page, 6); + }); + + it('should successfully filter on run types', async () => { + await pressElement(page, '.runType-filter .dropdown-trigger'); + await pressElement(page, '#run-types-dropdown-option-2', true); + await pressElement(page, '#run-types-dropdown-option-14', true); + await waitForTableLength(page, 5); + }); it('should successfully filter on beam mode', async () => { await pressElement(page, '.beamModes-filter .dropdown-trigger'); @@ -735,239 +735,239 @@ module.exports = () => { await waitForTableLength(page, 6); }); - // it('should successfully filter on nDetectors', async () => { - // await expectInputValue(page, '#nDetectors-operator', '='); - - // await page.select('#nDetectors-operator', '<='); - // await fillInput(page, '#nDetectors-operand', '1', ['change']); - // await waitForTableLength(page, 6); - - // const nDetectorsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { - // const rowId = row.id; - // return document.querySelector(`#${rowId}-detectors .nDetectors-badge`)?.innerText; - // })); - - // // The nDetectors can be null if the detectors' field is null but the nDetectors is not, which can be added in tests data - // expect(nDetectorsList.every((nDetectors) => parseInt(nDetectors, 10) <= 1 || nDetectors === null)).to.be.true; - // }); - - // it('should successfully filter on nFLPs', async () => { - // await expectInputValue(page, '#nFlps-operator', '='); - - // await page.select('#nFlps-operator', '<='); - // await fillInput(page, '#nFlps-operand', '10', ['change']); - // await waitForTableLength(page, 5); - - // const nFlpsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { - // const rowId = row.id; - // return document.querySelector(`#${rowId}-nFlps-text`)?.innerText; - // })); - // expect(nFlpsList.every((nFlps) => parseInt(nFlps, 10) <= 10)).to.be.true; - // }); - - // it('should successfully filter on nEPNs', async () => { - // await expectInputValue(page, '#nEpns-operator', '='); - - // await page.select('#nEpns-operator', '<='); - // await fillInput(page, '#nEpns-operand', '10', ['change']); - // await waitForTableLength(page, 5); - - // await expectColumnValues(page, 'nEpns', ['10', '10', 'OFF', 'OFF', '10']); - // }); - - // it('should successfully filter on EPN on/off', async () => { - // await pressElement(page, '#epnFilterRadioOFF', true); - // await waitForTableLength(page, 2); - // }); - - // it('should successfully filter by EOR Reason types', async () => { - // // Expect the EOR filter to exist - // await page.waitForSelector('#eorCategories'); - // await page.waitForSelector('#eorTitles'); - - // // Select the EOR reason category DETECTORS - // const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); - // await page.select('#eorCategories', 'DETECTORS'); - // await waitForTableLength(page, 3, undefined, oldTable); - // await page.waitForSelector('#eorTitles option'); - // let detectorTitleElements = await page.$$('#eorTitles option'); - // expect(detectorTitleElements).has.lengthOf(3); - - // // The titles dropdown should have updated - // const detectorTitles = await Promise.all(detectorTitleElements - // .map(async (element) => (await element.getProperty('value')).jsonValue())); - // expect(detectorTitles).deep.to.equal(['', 'CPV', 'TPC']); - - // /* - // * The correct number of runs should be displayed in the table. - // * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS' - // */ - // let eorReasons = await page.$$('table td[id$="eorReasons"]'); - // expect(eorReasons).has.lengthOf(3); - - // let eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); - - // let allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS')); - // expect(allTextsContainDetectors).to.be.true; - - // // Select the EOR reason title CPV - // await page.select('#eorTitles', 'CPV'); - // await waitForTableLength(page, 2); - - // /* - // * The correct number of runs should be displayed in the table. - // * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS - CPV' - // */ - // eorReasons = await page.$$('table td[id$="eorReasons"]'); - // expect(eorReasons).has.lengthOf(2); - - // eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); - - // allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS - CPV')); - // expect(allTextsContainDetectors).to.be.true; - - // // Reset filters. There should be a single blank option in the EOR titles dropdown - // await resetFilters(page) - // await waitForTableLength(page, 10); - // detectorTitleElements = await page.$$('#eorTitles option'); - // expect(detectorTitleElements).has.lengthOf(1); - - // // There should be many items in the run details table - // eorReasons = await page.$$('table td[id$="eorReasons"]'); - // expect(eorReasons.length).to.be.greaterThan(3); - // }); - - // it('should correctly filter by EOR reason description', async () => { - // // Expect there to be one result that contains a certain description - // const descriptionInput = 'some'; - // await fillInput(page, '#eorDescription', descriptionInput, ['change']); - // await waitForTableLength(page, 2); - - // let eorReasons = await page.$$('table td[id$="eorReasons"]'); - // expect(eorReasons).has.lengthOf(2); - // const eorReasonText = await (await eorReasons[0].getProperty('innerText')).jsonValue(); - // expect(eorReasonText.toLowerCase()).to.include(descriptionInput); - - // // Assuming this result had the category DETECTORS, when we select a different category it should disappear. - // await page.select('#eorCategories', 'OTHER'); - // await waitForEmptyTable(page); - // eorReasons = await page.$$('table td[id$="eorReasons"]'); - // expect(eorReasons).has.lengthOf(0); - - // // When we reset the filters, the input field should be empty - // await resetFilters(page); - // await waitForTableLength(page, 10); - // eorReasons = await page.$$('table td[id$="eorReasons"]'); - // expect(eorReasons.length).to.be.greaterThan(1); - - // await expectInputValue(page, '#eorDescription', ''); - // }); + it('should successfully filter on nDetectors', async () => { + await expectInputValue(page, '#nDetectors-operator', '='); + + await page.select('#nDetectors-operator', '<='); + await fillInput(page, '#nDetectors-operand', '1', ['change']); + await waitForTableLength(page, 6); + + const nDetectorsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { + const rowId = row.id; + return document.querySelector(`#${rowId}-detectors .nDetectors-badge`)?.innerText; + })); + + // The nDetectors can be null if the detectors' field is null but the nDetectors is not, which can be added in tests data + expect(nDetectorsList.every((nDetectors) => parseInt(nDetectors, 10) <= 1 || nDetectors === null)).to.be.true; + }); + + it('should successfully filter on nFLPs', async () => { + await expectInputValue(page, '#nFlps-operator', '='); + + await page.select('#nFlps-operator', '<='); + await fillInput(page, '#nFlps-operand', '10', ['change']); + await waitForTableLength(page, 5); + + const nFlpsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { + const rowId = row.id; + return document.querySelector(`#${rowId}-nFlps-text`)?.innerText; + })); + expect(nFlpsList.every((nFlps) => parseInt(nFlps, 10) <= 10)).to.be.true; + }); + + it('should successfully filter on nEPNs', async () => { + await expectInputValue(page, '#nEpns-operator', '='); + + await page.select('#nEpns-operator', '<='); + await fillInput(page, '#nEpns-operand', '10', ['change']); + await waitForTableLength(page, 5); + + await expectColumnValues(page, 'nEpns', ['10', '10', 'OFF', 'OFF', '10']); + }); + + it('should successfully filter on EPN on/off', async () => { + await pressElement(page, '#epnFilterRadioOFF', true); + await waitForTableLength(page, 2); + }); + + it('should successfully filter by EOR Reason types', async () => { + // Expect the EOR filter to exist + await page.waitForSelector('#eorCategories'); + await page.waitForSelector('#eorTitles'); + + // Select the EOR reason category DETECTORS + const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); + await page.select('#eorCategories', 'DETECTORS'); + await waitForTableLength(page, 3, undefined, oldTable); + await page.waitForSelector('#eorTitles option'); + let detectorTitleElements = await page.$$('#eorTitles option'); + expect(detectorTitleElements).has.lengthOf(3); + + // The titles dropdown should have updated + const detectorTitles = await Promise.all(detectorTitleElements + .map(async (element) => (await element.getProperty('value')).jsonValue())); + expect(detectorTitles).deep.to.equal(['', 'CPV', 'TPC']); + + /* + * The correct number of runs should be displayed in the table. + * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS' + */ + let eorReasons = await page.$$('table td[id$="eorReasons"]'); + expect(eorReasons).has.lengthOf(3); + + let eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); + + let allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS')); + expect(allTextsContainDetectors).to.be.true; + + // Select the EOR reason title CPV + await page.select('#eorTitles', 'CPV'); + await waitForTableLength(page, 2); + + /* + * The correct number of runs should be displayed in the table. + * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS - CPV' + */ + eorReasons = await page.$$('table td[id$="eorReasons"]'); + expect(eorReasons).has.lengthOf(2); + + eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); + + allTextsContainDetectors = eorReasonTexts.every((text) => text.includes('DETECTORS - CPV')); + expect(allTextsContainDetectors).to.be.true; + + // Reset filters. There should be a single blank option in the EOR titles dropdown + await resetFilters(page) + await waitForTableLength(page, 10); + detectorTitleElements = await page.$$('#eorTitles option'); + expect(detectorTitleElements).has.lengthOf(1); + + // There should be many items in the run details table + eorReasons = await page.$$('table td[id$="eorReasons"]'); + expect(eorReasons.length).to.be.greaterThan(3); + }); + + it('should correctly filter by EOR reason description', async () => { + // Expect there to be one result that contains a certain description + const descriptionInput = 'some'; + await fillInput(page, '#eorDescription', descriptionInput, ['change']); + await waitForTableLength(page, 2); + + let eorReasons = await page.$$('table td[id$="eorReasons"]'); + expect(eorReasons).has.lengthOf(2); + const eorReasonText = await (await eorReasons[0].getProperty('innerText')).jsonValue(); + expect(eorReasonText.toLowerCase()).to.include(descriptionInput); + + // Assuming this result had the category DETECTORS, when we select a different category it should disappear. + await page.select('#eorCategories', 'OTHER'); + await waitForEmptyTable(page); + eorReasons = await page.$$('table td[id$="eorReasons"]'); + expect(eorReasons).has.lengthOf(0); + + // When we reset the filters, the input field should be empty + await resetFilters(page); + await waitForTableLength(page, 10); + eorReasons = await page.$$('table td[id$="eorReasons"]'); + expect(eorReasons.length).to.be.greaterThan(1); + + await expectInputValue(page, '#eorDescription', ''); + }); }) - // describe("Export", () => { - // const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; - - // before(() => goToPage(page, 'run-overview')); - - // beforeEach(async () => { - // await navigateToRunsOverview(page); - // await resetFilters(page); - // }) - - // it('should successfully display runs export button', async () => { - // await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); - // const runsExportButton = await page.$(EXPORT_RUNS_TRIGGER_SELECTOR); - // expect(runsExportButton).to.be.not.null; - // }); - - // it('should successfully display runs export modal on click on export button', async () => { - // let exportModal = await page.$('#export-data-modal'); - // expect(exportModal).to.be.null; - - // await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - // await page.waitForSelector('#export-data-modal', { timeout: 5000 }); - // exportModal = await page.$('#export-data-modal'); - - // expect(exportModal).to.not.be.null; - // }); - - // it('should successfully display information when export will be truncated', async () => { - // await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true); - - // const truncatedExportWarning = await page.waitForSelector('#export-data-modal #truncated-export-warning'); - // expect(await truncatedExportWarning.evaluate((warning) => warning.innerText)) - // .to - // .equal('The data export is limited to 100 entries, only the most recent data will be exported'); - // }); - - // it('should successfully display disabled runs export button when there is no runs available', async () => { - // await openFilteringPanel(page);; - - // // Type a fake run number to have no runs - // await fillInput(page, filterPanelRunNumbersInputSelector, '99999999999', ['change']); - // await openFilteringPanel(page);; - - // await page.waitForSelector(`${EXPORT_RUNS_TRIGGER_SELECTOR}:disabled`); - // }); - - // it('should successfully export filtered runs', async () => { - // const targetFileName = 'data.json'; - - // // First export - // await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true) - // await page.waitForSelector('#export-data-modal', { timeout: 5000 }); - // await page.waitForSelector('#send:disabled'); - // await page.waitForSelector('#export-data-modal select.form-control'); - // await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); - // await page.waitForSelector('#send:enabled'); - // const exportButtonText = await page.$eval('#send', (button) => button.innerText); - // expect(exportButtonText).to.be.eql('Export'); - - // { - // const downloadPath = await waitForDownload(page, () => pressElement(page, '#send', true)); - - // // Check download - // const downloadFilesNames = fs.readdirSync(downloadPath); - // expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); - // const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); - - // expect(runs).to.be.lengthOf(100); - // expect(runs.every(({ runQuality, runNumber, ...otherProps }) => - // runQuality && runNumber && Object.keys(otherProps).length === 0)).to.be.true; - // fs.unlinkSync(path.resolve(downloadPath, targetFileName)); - // } - - // // Second export - - // // Apply filtering - // await openFilteringPanel(page); + describe("Export", () => { + const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; + + before(() => goToPage(page, 'run-overview')); + + beforeEach(async () => { + await navigateToRunsOverview(page); + await resetFilters(page); + }) + + it('should successfully display runs export button', async () => { + await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); + const runsExportButton = await page.$(EXPORT_RUNS_TRIGGER_SELECTOR); + expect(runsExportButton).to.be.not.null; + }); + + it('should successfully display runs export modal on click on export button', async () => { + let exportModal = await page.$('#export-data-modal'); + expect(exportModal).to.be.null; + + await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); + await page.waitForSelector('#export-data-modal', { timeout: 5000 }); + exportModal = await page.$('#export-data-modal'); + + expect(exportModal).to.not.be.null; + }); + + it('should successfully display information when export will be truncated', async () => { + await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true); + + const truncatedExportWarning = await page.waitForSelector('#export-data-modal #truncated-export-warning'); + expect(await truncatedExportWarning.evaluate((warning) => warning.innerText)) + .to + .equal('The data export is limited to 100 entries, only the most recent data will be exported'); + }); + + it('should successfully display disabled runs export button when there is no runs available', async () => { + await openFilteringPanel(page);; + + // Type a fake run number to have no runs + await fillInput(page, filterPanelRunNumbersInputSelector, '99999999999', ['change']); + await openFilteringPanel(page);; + + await page.waitForSelector(`${EXPORT_RUNS_TRIGGER_SELECTOR}:disabled`); + }); + + it('should successfully export filtered runs', async () => { + const targetFileName = 'data.json'; + + // First export + await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true) + await page.waitForSelector('#export-data-modal', { timeout: 5000 }); + await page.waitForSelector('#send:disabled'); + await page.waitForSelector('#export-data-modal select.form-control'); + await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); + await page.waitForSelector('#send:enabled'); + const exportButtonText = await page.$eval('#send', (button) => button.innerText); + expect(exportButtonText).to.be.eql('Export'); + + { + const downloadPath = await waitForDownload(page, () => pressElement(page, '#send', true)); + + // Check download + const downloadFilesNames = fs.readdirSync(downloadPath); + expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); + const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); + + expect(runs).to.be.lengthOf(100); + expect(runs.every(({ runQuality, runNumber, ...otherProps }) => + runQuality && runNumber && Object.keys(otherProps).length === 0)).to.be.true; + fs.unlinkSync(path.resolve(downloadPath, targetFileName)); + } + + // Second export + + // Apply filtering + await openFilteringPanel(page); - // const filterInputSelectorPrefix = '#checkboxes-checkbox-'; - // const badFilterSelector = `${filterInputSelectorPrefix}bad`; - - // await openFilteringPanel(page);; - // await page.waitForSelector(badFilterSelector); - // await page.$eval(badFilterSelector, (element) => element.click()); - // await page.waitForSelector('tbody tr:nth-child(2)'); - // await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); - - // ///// Download - // await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - // await page.waitForSelector('#export-data-modal', { timeout: 5000 }); - - // await page.waitForSelector('#export-data-modal select.form-control', { timeout: 10000 }); - // await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); - - // { - // const downloadPath = await waitForDownload(page, () => pressElement(page, '#send:enabled', true)); - - // // Check download - // const downloadFilesNames = fs.readdirSync(downloadPath); - // expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); - // const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); - // expect(runs).to.have.all.deep.members([{ runNumber: 2, runQuality: 'bad' }, { runNumber: 1, runQuality: 'bad' }]); - // } - // }); - // }) + const filterInputSelectorPrefix = '#checkboxes-checkbox-'; + const badFilterSelector = `${filterInputSelectorPrefix}bad`; + + await openFilteringPanel(page);; + await page.waitForSelector(badFilterSelector); + await page.$eval(badFilterSelector, (element) => element.click()); + await page.waitForSelector('tbody tr:nth-child(2)'); + await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); + + ///// Download + await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); + await page.waitForSelector('#export-data-modal', { timeout: 5000 }); + + await page.waitForSelector('#export-data-modal select.form-control', { timeout: 10000 }); + await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); + + { + const downloadPath = await waitForDownload(page, () => pressElement(page, '#send:enabled', true)); + + // Check download + const downloadFilesNames = fs.readdirSync(downloadPath); + expect(downloadFilesNames.filter((name) => name === targetFileName)).to.be.lengthOf(1); + const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); + expect(runs).to.have.all.deep.members([{ runNumber: 2, runQuality: 'bad' }, { runNumber: 1, runQuality: 'bad' }]); + } + }); + }) }; From 09c65222ca4d2efcf33fd0ae04233181f24142eb Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Fri, 13 Feb 2026 13:34:46 +0100 Subject: [PATCH 20/23] remove beammode visibility --- lib/public/views/Runs/ActiveColumns/runsActiveColumns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 8448ff321f..eefe0f006f 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -152,7 +152,7 @@ export const runsActiveColumns = { }, beamModes: { name: 'Beam Mode', - visible: true, + visible: false, classes: 'cell-l f6 w-wrapped', format: formatNamedValue, From 94530f2659dd3e5fe5ded7d053f43153ffe7a3ad Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Mon, 16 Feb 2026 14:52:33 +0100 Subject: [PATCH 21/23] add tests for the new formatNamedValue component --- .../formatting/formatNamedValue.test.js | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/lib/public/utilities/formatting/formatNamedValue.test.js diff --git a/test/lib/public/utilities/formatting/formatNamedValue.test.js b/test/lib/public/utilities/formatting/formatNamedValue.test.js new file mode 100644 index 0000000000..d51bbf3343 --- /dev/null +++ b/test/lib/public/utilities/formatting/formatNamedValue.test.js @@ -0,0 +1,45 @@ +/** + * @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. + */ + +const { expect } = require('chai'); + +let formatNamedValue; + +module.exports = () => { + before(async () => { + ({ formatNamedValue } = await import('../../../../../lib/public/utilities/formatting/formatNamedValue.js')); + }); + + it('should return value.name when value is an object with a name property', () => { + expect(formatNamedValue({ name: 'John Doe' })).to.equal('John Doe'); + expect(formatNamedValue({ name: 'Test', id: 123 })).to.equal('Test'); + }); + + it('should return value when value has no name property', () => { + expect(formatNamedValue('Plain String')).to.equal('Plain String'); + expect(formatNamedValue(42)).to.equal(42); + expect(formatNamedValue(true)).to.equal(true); + expect(formatNamedValue({ id: 1 })).to.deep.equal({ id: 1 }); + }); + + it('should return "-" when value is null or undefined', () => { + expect(formatNamedValue(null)).to.equal('-'); + expect(formatNamedValue(undefined)).to.equal('-'); + }); + + it('should correctly handle falsy but defined values', () => { + expect(formatNamedValue('')).to.equal(''); + expect(formatNamedValue(0)).to.equal(0); + expect(formatNamedValue(false)).to.equal(false); + }); +}; From 560585f89b69544011d24c135de8866c0a3ff07d Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Mon, 16 Feb 2026 16:09:19 +0100 Subject: [PATCH 22/23] add test to the index --- test/lib/public/utilities/formatting/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/lib/public/utilities/formatting/index.js b/test/lib/public/utilities/formatting/index.js index c60cb34481..345cd74620 100644 --- a/test/lib/public/utilities/formatting/index.js +++ b/test/lib/public/utilities/formatting/index.js @@ -12,7 +12,9 @@ */ const formatRunDurationTest = require('./formatRunDuration.test.js'); +const formatNamedValuesTest = require('./formatNamedValue.test.js') module.exports = () => { describe('formatRunDurationTest', formatRunDurationTest); + describe('formatNamedValuesTest', formatNamedValuesTest); }; From 1e5f8b0a1d5a73fb806248c5a229be28ce450bd4 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Mon, 16 Feb 2026 16:54:04 +0100 Subject: [PATCH 23/23] fix edge case in formatNamedValue.js --- lib/public/utilities/formatting/formatNamedValue.js | 6 +++++- .../public/utilities/formatting/formatNamedValue.test.js | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/public/utilities/formatting/formatNamedValue.js b/lib/public/utilities/formatting/formatNamedValue.js index fc9750cfd0..e728e5b2fa 100644 --- a/lib/public/utilities/formatting/formatNamedValue.js +++ b/lib/public/utilities/formatting/formatNamedValue.js @@ -19,5 +19,9 @@ * @returns {string} The name or id or a '-' if null */ export function formatNamedValue(value) { - return value?.name ?? value ?? '-'; + if (['string', 'number', 'boolean'].includes(typeof value)) { + return value; + } + + return value?.name ?? '-'; } diff --git a/test/lib/public/utilities/formatting/formatNamedValue.test.js b/test/lib/public/utilities/formatting/formatNamedValue.test.js index d51bbf3343..cf9f29865e 100644 --- a/test/lib/public/utilities/formatting/formatNamedValue.test.js +++ b/test/lib/public/utilities/formatting/formatNamedValue.test.js @@ -25,11 +25,10 @@ module.exports = () => { expect(formatNamedValue({ name: 'Test', id: 123 })).to.equal('Test'); }); - it('should return value when value has no name property', () => { + it('should return value when value is a string, number or a boolean', () => { expect(formatNamedValue('Plain String')).to.equal('Plain String'); expect(formatNamedValue(42)).to.equal(42); expect(formatNamedValue(true)).to.equal(true); - expect(formatNamedValue({ id: 1 })).to.deep.equal({ id: 1 }); }); it('should return "-" when value is null or undefined', () => { @@ -37,6 +36,11 @@ module.exports = () => { expect(formatNamedValue(undefined)).to.equal('-'); }); + it('should return "-" when value.name is null or undefined', () => { + expect(formatNamedValue({name: undefined})).to.equal('-'); + expect(formatNamedValue({name: null})).to.equal('-'); + }); + it('should correctly handle falsy but defined values', () => { expect(formatNamedValue('')).to.equal(''); expect(formatNamedValue(0)).to.equal(0);