diff --git a/lib/public/app.css b/lib/public/app.css index cccddff024..92a684e58a 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -962,6 +962,34 @@ label { width: 8rem; } +/* + * Columns of the environments overview: all of them but the detectors, which take the remaining width, have a fixed width fitting their + * content + */ +.w-environments-id-column { + width: 10rem; +} + +.w-environments-status-column { + width: 12rem; +} + +.w-environments-created-column { + width: 12rem; +} + +.w-environments-history-column { + width: 18rem; +} + +.w-environments-configuration-column { + width: 11rem; +} + +.w-environments-runs-column { + width: 8rem; +} + /* * Beam types of the LHC fills overview, by what collides: only shapes and neutral shades are used, as colors are reserved to states */ diff --git a/lib/public/components/common/formatting/dataTakingConfiguration.js b/lib/public/components/common/formatting/dataTakingConfiguration.js new file mode 100644 index 0000000000..1915c6a4ce --- /dev/null +++ b/lib/public/components/common/formatting/dataTakingConfiguration.js @@ -0,0 +1,52 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h } from '/js/src/index.js'; +import { labelledLine } from './labelledLine.js'; + +/** + * Format the number of EPNs: the count if known, ON if EPNs are used without a known count, OFF otherwise + * + * @param {boolean} epn true if EPNs are used + * @param {number|null} nEpns the number of EPNs + * @return {string|number} the formatted value + */ +const formatEpnsCount = (epn, nEpns) => { + if (!epn) { + return 'OFF'; + } + return typeof nEpns === 'number' ? nEpns : 'ON'; +}; + +/** + * Display the data taking configuration of a run or of an environment: the number of EPNs and FLPs, the DCS state and the trigger, two by line + * + * @param {string} idPrefix the prefix of the ids of the elements (e.g. `row123`) + * @param {object} configuration the configuration to display + * @param {boolean} configuration.epn true if EPNs are used + * @param {number|null} configuration.nEpns the number of EPNs + * @param {number|null} configuration.nFlps the number of FLPs + * @param {boolean} configuration.dcs true if DCS is enabled + * @param {string|null} configuration.triggerValue the trigger (OFF, LTU or CTP) + * @return {Component} the configuration + */ +export const dataTakingConfiguration = (idPrefix, { epn, nEpns, nFlps, dcs, triggerValue }) => h('', [ + h(`#${idPrefix}-nodes.flex-row.g2`, [ + labelledLine(`${idPrefix}-nEpns`, 'EPNs', formatEpnsCount(epn, nEpns)), + labelledLine(`${idPrefix}-nFlps`, 'FLPs', nFlps ?? '-'), + ]), + h(`#${idPrefix}-states.flex-row.g2`, [ + labelledLine(`${idPrefix}-dcs`, 'DCS', dcs ? 'ON' : 'OFF'), + labelledLine(`${idPrefix}-triggerValue`, 'TRG', triggerValue || '-'), + ]), +]); diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index b7bb400bc1..96f963f6b4 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -14,30 +14,45 @@ import { CopyToClipboardComponent, h } from '/js/src/index.js'; import { iconPlus } from '/js/src/icons.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; +import { dataTakingConfiguration } from '../../../components/common/formatting/dataTakingConfiguration.js'; +import { linkWithActionsDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js'; import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { displayEnvironmentStatus } from '../format/displayEnvironmentStatus.js'; -import { buttonLinkWithDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/buttonLinkWithDropdown.js'; import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js';; -import { coloredEnvironmentStatusComponent } from '../ColoredEnvironmentStatusComponent.js'; import { environmentStatusHistoryLegendComponent } from '../../../components/environments/environmentStatusHistoryComponent.js'; +import { overflowBalloon } from '../../../components/common/popover/overflowBalloon.js'; import { infoTooltip } from '../../../components/common/popover/infoTooltip.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; -import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; +import { formatEnvironmentStatusHistory } from '../format/formatEnvironmentStatusHistory.js'; +import { parseEnvironmentConfiguration } from '../format/parseEnvironmentConfiguration.js'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; /** - * List of active columns for a generic Environments component + * List of active columns for the environments overview + * + * The current status is displayed with its message and the status history with the time spent running by the environment */ export const environmentsActiveColumns = { id: { - name: 'Id', - size: 'w-10 f6 w-wrapped', + name: 'Environment', + size: 'w-environments-id-column', + classes: 'f6', visible: true, primary: true, - format: (environmentId, { status, runs }) => buttonLinkWithDropdown( + noEllipsis: true, + + /** + * Display the id of the environment as a link with its actions + * + * @param {string} environmentId the id of the environment + * @param {Environment} environment the environment + * @return {Component} the environment id + */ + format: (environmentId, { status, runs }) => linkWithActionsDropdown( + `row${environmentId}-id`, environmentId, 'env-details', { environmentId }, @@ -54,7 +69,6 @@ export const environmentsActiveColumns = { { id: 'add-log-link', class: 'btn btn-primary h2' }, ), ], - '125px', ), /** @@ -66,36 +80,42 @@ export const environmentsActiveColumns = { */ filter: ({ filteringModel }) => textInputFilter(filteringModel, 'ids', 'e.g. CmCvjNbg, TDI59So3d...'), }, - runs: { - name: 'Runs', + status: { + name: 'Current Status', visible: true, sortable: false, - size: 'w-10', - format: formatRunsList, - balloon: true, + size: 'w-environments-status-column', + classes: 'f6', + noEllipsis: true, /** - * Run numbers filter component + * Display the current status of the environment, with its message below it * - * @param {object} environmentOverviewModel the overview model - * @param {FilteringModel} environmentOverviewModel.filteringModel the filtering model + * @param {*} _ unused, the status is read from the environment + * @param {Environment} environment the environment + * @return {Component} the current status and its message + */ + format: (_, environment) => h('', [ + h(`#row${environment.id}-currentStatus`, displayEnvironmentStatus(environment)), + h(`#row${environment.id}-statusMessage.f7.gray-darker`, overflowBalloon(environment.statusMessage || '-')), + ]), + + /** + * Status filter component + * + * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'runNumbers', 'e.g. 553203, 553221, ...'), - }, - updatedAt: { - name: 'Last Update', - visible: true, - sortable: false, - size: 'w-10', - format: (timestamp) => formatTimestamp(timestamp, false), + filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), }, createdAt: { name: 'Created At', visible: true, sortable: false, - size: 'w-10', - format: (timestamp) => formatTimestamp(timestamp, false), + size: 'w-environments-created-column', + classes: 'f6', + noEllipsis: true, + format: (timestamp) => formatTimestamp(timestamp, true), /** * CreatedAt filter component @@ -105,41 +125,84 @@ export const environmentsActiveColumns = { */ filter: (environmentOverviewModel) => timeRangeFilter(environmentOverviewModel.filteringModel.get('created')), }, - status: { - name: 'Current Status', + historyItems: { + name: h('.flex-row.g2.items-center', ['Status History', infoTooltip(environmentStatusHistoryLegendComponent())]), visible: true, sortable: false, - size: 'w-10', + size: 'w-environments-history-column', + classes: 'f6', noEllipsis: true, - format: (_, environment) => displayEnvironmentStatus(environment), + format: (historyItems, { id }) => formatEnvironmentStatusHistory(historyItems, id), /** - * Status filter component + * Status history filter component * - * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model + * @param {object} environmentOverviewModel the overview model + * @param {FilteringModel} environmentOverviewModel.filteringModel the filtering model * @return {Component} the filter component */ - filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), + filter: ({ filteringModel }) => textInputFilter(filteringModel, 'statusHistory', 'e.g. D-R-X'), }, - historyItems: { - name: h('.flex-row.g2.items-center', ['Status History', infoTooltip(environmentStatusHistoryLegendComponent())]), + runs: { + name: 'Runs', visible: true, sortable: false, - size: 'w-20', - format: (historyItems) => historyItems - .flatMap(({ status }) => [ - '-', - coloredEnvironmentStatusComponent(status, StatusAcronym[status]), - ]).slice(1), + size: 'w-environments-runs-column', + classes: 'f6', + format: formatRunsList, balloon: true, /** - * Status history filter component + * Run numbers filter component * * @param {object} environmentOverviewModel the overview model * @param {FilteringModel} environmentOverviewModel.filteringModel the filtering model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'statusHistory', 'e.g. D-R-X'), + filter: ({ filteringModel }) => textInputFilter(filteringModel, 'runNumbers', 'e.g. 553203, 553221, ...'), + }, + configuration: { + name: 'Configuration', + visible: true, + sortable: false, + size: 'w-environments-configuration-column', + classes: 'f6', + noEllipsis: true, + + /** + * Display the number of EPNs and FLPs, the DCS state and the trigger of the environment, two by line, as for the runs + * + * @param {*} _ unused, the configuration is read from the environment + * @param {Environment} environment the environment + * @return {Component|string} the configuration, or a dash if the environment has no valid configuration + */ + format: (_, { id, rawConfiguration }) => { + const configuration = parseEnvironmentConfiguration(rawConfiguration); + return configuration ? dataTakingConfiguration(`row${id}`, configuration) : '-'; + }, + }, + detectors: { + name: 'Detectors', + visible: true, + sortable: false, + classes: 'f6', + noEllipsis: true, + + /** + * Display the number of detectors of the environment followed by their names, on a single line + * + * @param {*} _ unused, the detectors are read from the configuration of the environment + * @param {Environment} environment the environment + * @return {Component|string} the detectors, or a dash if the environment has none + */ + format: (_, { id, rawConfiguration }) => { + const detectors = parseEnvironmentConfiguration(rawConfiguration)?.detectors ?? []; + return detectors.length > 0 + ? h(`#row${id}-detectors`, overflowBalloon(h('', [ + h('.badge.bg-gray-light.mr1', detectors.length), + detectors.join(', '), + ]))) + : '-'; + }, }, }; diff --git a/lib/public/views/Environments/Overview/environmentOverviewComponent.js b/lib/public/views/Environments/Overview/environmentOverviewComponent.js index df8f5a332d..20ff5eac6e 100644 --- a/lib/public/views/Environments/Overview/environmentOverviewComponent.js +++ b/lib/public/views/Environments/Overview/environmentOverviewComponent.js @@ -19,9 +19,9 @@ import { paginationComponent } from '../../../components/Pagination/paginationCo import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { warningComponent } from '../../../components/common/messages/warningComponent.js'; -const TABLEROW_HEIGHT = 58; +const TABLEROW_HEIGHT = 51; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; -const PAGE_USED_HEIGHT = 181; +const PAGE_USED_HEIGHT = 215; /** * The shows the environment table diff --git a/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js new file mode 100644 index 0000000000..29faf084a6 --- /dev/null +++ b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js @@ -0,0 +1,77 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h } from '/js/src/index.js'; +import { coloredEnvironmentStatusComponent } from '../ColoredEnvironmentStatusComponent.js'; +import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; +import { tooltip } from '../../../components/common/popover/tooltip.js'; +import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; +import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; + +const RUNNING_STATUS = 'RUNNING'; + +/** + * Compute the time spent in each status of the history (the duration of the last one is unknown), the total time spent running and the time + * between the first and the last status + * + * @param {{status: string, createdAt: number}[]} historyItems the history items of the environment, sorted + * @return {{durations: (number|null)[], runningDuration: number, totalDuration: number}} the durations + */ +const computeDurations = (historyItems) => { + const durations = historyItems.map(({ createdAt }, index) => { + const nextItem = historyItems[index + 1]; + return nextItem ? nextItem.createdAt - createdAt : null; + }); + const runningDuration = historyItems.reduce( + (total, { status }, index) => status === RUNNING_STATUS ? total + (durations[index] ?? 0) : total, + 0, + ); + const totalDuration = historyItems.length > 1 ? historyItems[historyItems.length - 1].createdAt - historyItems[0].createdAt : 0; + + return { durations, runningDuration, totalDuration }; +}; + +/** + * Display the status history of an environment on two lines: the acronym of each status in its color, separated by chevrons, then the total + * time of the environment and the time it spent running. Hovering a status displays its message, the time it was reached and the time spent + * in it + * + * @param {{status: string, statusMessage: string, createdAt: number}[]} historyItems the history items of the environment, sorted + * @param {string} id the id of the environment, used to generate the ids of the elements + * @return {Component} the status history + */ +export const formatEnvironmentStatusHistory = (historyItems, id) => { + const { durations, runningDuration, totalDuration } = computeDurations(historyItems); + + return h('', [ + h( + `#row${id}-statusChain.flex-row.items-baseline.g1`, + historyItems.flatMap(({ status, statusMessage, createdAt }, index) => [ + h('span.gray-dark', '\u203A'), + tooltip( + h('strong', coloredEnvironmentStatusComponent(status, StatusAcronym[status])), + h('.flex-column', [ + h('strong', status), + statusMessage && h('', statusMessage), + h('.f7', formatTimestamp(createdAt, true)), + durations[index] !== null && h('.f7', `Stayed ${formatDuration(durations[index])}`), + ]), + ), + ]).slice(1), + ), + h(`#row${id}-statusSummary.flex-row.g3.f7.gray-darker`, [ + h('span', ['Total ', h('span.black', formatDuration(totalDuration))]), + h('span', ['Running ', h('span.black', formatDuration(runningDuration))]), + ]), + ]); +}; diff --git a/lib/public/views/Environments/format/parseEnvironmentConfiguration.js b/lib/public/views/Environments/format/parseEnvironmentConfiguration.js new file mode 100644 index 0000000000..5751442efd --- /dev/null +++ b/lib/public/views/Environments/format/parseEnvironmentConfiguration.js @@ -0,0 +1,93 @@ +/** + * @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 { TriggerValue } from '../../../domain/enums/TriggerValue.js'; + +/** + * Parse a value of the environment configuration that may be a JSON serialized list (e.g. `["FT0", "ITS"]`) or an already parsed one + * + * @param {*} value the value to parse + * @return {string[]} the list + */ +const parseList = (value) => { + if (Array.isArray(value)) { + return [...value]; + } + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + +/** + * Parse a boolean value of the environment configuration, that may be serialized (e.g. `"true"`) + * + * @param {*} value the value to parse + * @return {boolean} the boolean, false if the value is missing or invalid + */ +const parseBoolean = (value) => { + try { + return JSON.parse(value) === true; + } catch { + return false; + } +}; + +/** + * @typedef EnvironmentConfigurationSummary + * @property {string[]} detectors the detectors of the environment + * @property {boolean} epn true if EPNs are used + * @property {number|null} nEpns the number of EPNs + * @property {number|null} nFlps the number of FLPs (hosts, with the CTP readout host if it is enabled) + * @property {boolean} dcs true if DCS is enabled + * @property {string} triggerValue the trigger (OFF, LTU or CTP), computed as for the runs started in the environment + */ + +/** + * Extract the most relevant information of the raw configuration of an environment + * + * @param {string|null} rawConfiguration the raw configuration of the environment (JSON serialized) + * @return {EnvironmentConfigurationSummary|null} the summary, or null if there is no valid configuration + */ +export const parseEnvironmentConfiguration = (rawConfiguration) => { + if (!rawConfiguration) { + return null; + } + + try { + const configuration = JSON.parse(rawConfiguration); + const hosts = parseList(configuration.hosts); + if (parseBoolean(configuration.ctp_readout_enabled) && configuration.ctp_readout_host) { + hosts.push(configuration.ctp_readout_host); + } + const nEpns = parseInt(configuration.odc_n_epns, 10); + + let triggerValue = TriggerValue.Off; + if (parseBoolean(configuration.trg_enabled)) { + triggerValue = parseBoolean(configuration.trg_global_run_enabled) ? TriggerValue.CTP : TriggerValue.LTU; + } + + return { + detectors: parseList(configuration.detectors), + epn: parseBoolean(configuration.odc_enabled), + nEpns: isNaN(nEpns) ? null : nEpns, + nFlps: hosts.length, + dcs: parseBoolean(configuration.dcs_enabled), + triggerValue, + }; + } catch { + return null; + } +}; diff --git a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js index 596588ea03..4eca6854d6 100644 --- a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js @@ -14,6 +14,7 @@ import { h } from '/js/src/index.js'; import { runActionsComponents, runsActiveColumns } from './runsActiveColumns.js'; import { profiles } from '../../../components/common/table/profiles.js'; +import { dataTakingConfiguration } from '../../../components/common/formatting/dataTakingConfiguration.js'; import { labelledLine } from '../../../components/common/formatting/labelledLine.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { formatRunStart } from '../format/formatRunStart.js'; @@ -59,20 +60,6 @@ const TIMING_LINE = { labelClass: 'timing-label' }; */ const hiddenColumn = (key) => ({ ...runsActiveColumns[key], visible: false, profiles: [profiles.none] }); -/** - * Format the number of EPNs of a run: the count if known, ON if EPNs were used without a known count, OFF otherwise - * - * @param {boolean} epn true if the run used EPNs - * @param {number|null} nEpns the number of EPNs - * @return {string|number} the formatted value - */ -const formatEpnsCount = (epn, nEpns) => { - if (!epn) { - return 'OFF'; - } - return typeof nEpns === 'number' ? nEpns : 'ON'; -}; - /** * Display the definition and the quality of the run on one line, separated by a dash, with the calibration status of calibration runs on * its own line below @@ -181,16 +168,7 @@ export const runsOverviewActiveColumns = { * @param {Run} run the run * @return {Component} the EPNs, FLPs, DCS and trigger */ - format: (_, { id, epn, nEpns, nFlps, dcs, triggerValue }) => h('', [ - h(`#row${id}-nodes.flex-row.g2`, [ - labelledLine(`row${id}-nEpns`, 'EPNs', formatEpnsCount(epn, nEpns)), - labelledLine(`row${id}-nFlps`, 'FLPs', nFlps ?? '-'), - ]), - h(`#row${id}-states.flex-row.g2`, [ - labelledLine(`row${id}-dcs`, 'DCS', dcs ? 'ON' : 'OFF'), - labelledLine(`row${id}-triggerValue`, 'TRG', triggerValue || '-'), - ]), - ]), + format: (_, run) => dataTakingConfiguration(`row${run.id}`, run), }, detectors: { ...runsActiveColumns.detectors, diff --git a/test/public/envs/overview.test.js b/test/public/envs/overview.test.js index d2c58d927c..38756dee26 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -17,11 +17,10 @@ const { pressElement, checkColumnBalloon, expectLink, - validateTableData, expectInnerText, + expectInnerTextTo, expectUrlParams, waitForNavigation, - getInnerText, waitForTableLength, getPopoverSelector, goToPage, @@ -68,16 +67,36 @@ module.exports = () => { const statusNames = new Set(Object.keys(StatusAcronym)); - // eslint-disable-next-line jsdoc/require-param - const checkDate = (date) => !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY hh:mm:ss')); - const tableDataValidators = { - id: (id) => /[A-Za-z0-9]+/.test(id), - runs: (runs) => runs === '-' || runs.split(',').every((run) => !isNaN(run)), - updatedAt: checkDate, - status: (currentStatus) => statusNames.has(currentStatus), - historyItems: (history) => history.split('-').every((statusAcronym) => STATUS_ACRONYMS.includes(statusAcronym)), - }; - await validateTableData(page, new Map(Object.entries(tableDataValidators))); + const checkDate = (date) => !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY, HH:mm:ss')); + const durationRegex = /^\d{2,}:\d{2}:\d{2}$/; + + await page.waitForSelector('tbody tr:nth-of-type(1) [id$="-statusChain"]'); + const rows = await page.$$eval('tbody tr', (trs) => trs.map((tr) => { + const text = (suffix) => tr.querySelector(`[id$="-${suffix}"]`)?.innerText; + return { + id: text('id'), + currentStatus: text('currentStatus'), + createdAt: text('createdAt'), + statusChain: tr.querySelector('[id$="-statusChain"]')?.textContent, + total: tr.querySelector('[id$="-statusSummary"] span:nth-child(1) span')?.innerText, + running: tr.querySelector('[id$="-statusSummary"] span:nth-child(2) span')?.innerText, + runs: text('runs'), + }; + })); + + expect(rows).to.have.length.greaterThan(0); + for (const row of rows) { + expect(row.id, `id of ${JSON.stringify(row)}`).to.match(/^[A-Za-z0-9]+$/); + expect(statusNames.has(row.currentStatus), `status of ${row.id}`).to.be.true; + expect(checkDate(row.createdAt), `creation date of ${row.id}: ${row.createdAt}`).to.be.true; + expect( + row.statusChain.split('\u203A').every((statusAcronym) => STATUS_ACRONYMS.includes(statusAcronym)), + `status history of ${row.id}: ${row.statusChain}`, + ).to.be.true; + expect(row.total, `total time of ${row.id}`).to.match(durationRegex); + expect(row.running, `running time of ${row.id}`).to.match(durationRegex); + expect(row.runs === '-' || row.runs.split(',').every((run) => !isNaN(run)), `runs of ${row.id}: ${row.runs}`).to.be.true; + } }); it('Should display the correct items counter at the bottom of the page', async () => { @@ -87,57 +106,39 @@ module.exports = () => { }); it('Should have balloon on runs column', async () => { - await checkColumnBalloon(page, 1, 2); - await checkColumnBalloon(page, 1, 6); + await checkColumnBalloon(page, 1, 5); }); it('Should have correct status color in the overview page', async () => { - /** - * Check that a given cell of the given column displays the correct color depending on the status - * - * @param {number} rowIndex the index of the row to look for status color - * @param {number} columnIndex the index of the column to look for status color - * @returns {Promise} void promise - */ - const checkEnvironmentStatusColor = async (rowIndex, columnIndex) => { - const cellSelector = `tbody tr:nth-of-type(${rowIndex}) td:nth-of-type(${columnIndex})`; - const cell = await page.waitForSelector(cellSelector); - const cellContent = await getInnerText(cell); - - switch (cellContent) { - case 'RUNNING': - await page.waitForSelector(`${cellSelector}.success`); - break; - case 'ERROR': - await page.waitForSelector(`${cellSelector}.danger`); - break; - case 'CONFIGURED': - await page.waitForSelector(`${cellSelector}.primary`); - break; - case 'DONE': - await page.waitForSelector(`${cellSelector}.black`); - break; - case 'DEPLOYED': - await page.waitForSelector(`${cellSelector}.gray`); - break; - case 'PENDING': - await page.waitForSelector(`${cellSelector}.gray`); - break; - case 'STANDBY': - await page.waitForSelector(`${cellSelector}.gray`); - break; - case 'UNKNOWN': - await page.waitForSelector(`${cellSelector}.gray-dark`); - break; - } - + const expectedColors = { + CmCvjNbg: ['RUNNING', 'success'], + EIDO13i3D: ['ERROR', 'danger'], + TDI59So3d: ['DONE', 'black'], }; - await checkEnvironmentStatusColor(1, 4); - await checkEnvironmentStatusColor(2, 4); - await checkEnvironmentStatusColor(3, 4); - await checkEnvironmentStatusColor(6, 4); - await checkEnvironmentStatusColor(9, 4); + for (const [environmentId, [status, colorClass]] of Object.entries(expectedColors)) { + await expectInnerText(page, `#row${environmentId}-currentStatus`, status); + await page.waitForSelector(`#row${environmentId}-currentStatus span.${colorClass}`); + } + }); + + it('Should display the message of the current status below it', async () => { + await expectInnerText(page, '#rowEIDO13i3D-statusMessage', 'This one is really broken'); + }); + + it('Should display the total and running times of the status history, and its statuses on one line', async () => { + // TDI59So3d: configured, running during one hour, deployed then done + await expectInnerText(page, '#rowTDI59So3d-statusChain', 'C\n\u203A\nR\n\u203A\nD\n\u203A\nX'); + await expectInnerText(page, '#rowTDI59So3d-statusSummary', 'Total 02:55:00\nRunning 01:00:00'); + }); + + it('Should display the configuration of the environment, as for the runs, and its detectors', async () => { + // The CTP readout host is counted in the FLPs + await expectInnerText(page, '#rowCmCvjNbg-nEpns', '2'); + await expectInnerText(page, '#rowCmCvjNbg-nFlps', '3'); + await expectInnerText(page, '#rowCmCvjNbg-dcs', 'ON'); + await expectInnerText(page, '#rowCmCvjNbg-triggerValue', 'CTP'); + await expectInnerTextTo(page, '#rowCmCvjNbg-detectors', (text) => text.includes('FT0, ITS') && text.startsWith('2')); }); it('can set how many environments are available per page', async () => {