diff --git a/QualityControl/public/common/chevronButton.js b/QualityControl/public/common/chevronButton.js new file mode 100644 index 000000000..ab521c89d --- /dev/null +++ b/QualityControl/public/common/chevronButton.js @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h, iconChevronLeft, iconChevronRight } from '/js/src/index.js'; + +/** + * @typedef {object} ChevronButtonOptions + * @property {string} [id] - html id property. + * @property {string} [class] - html class property, multiple classes can be passed as a single space seperated string. + * @property {string} [title] - title shown on hover. + * @property {boolean} [isVisible=false] - shows right chevron if true, left chevron if false. + */ + +/** + * Chevron direction toggle button (forward/backward). + * Creates an anchor element that displays a right chevron icon if forward or a left chevron icon if backward. + * @param {() => void} onclick - Callback invoked when the button is clicked. + * @param {ChevronButtonOptions} options - Additional options for the button element + * @returns {vnode} - Chevron direction toggle button vnode. + * @example + * chevronButton( + * () => { + * objectViewModel.toggleObjectInfoVisible(); + * }, + * { + * isVisible: objectViewModel.getObjectInfoVisible(), + * title: 'Toggle object information visibility', + * }, + * ); + */ +export function chevronButton(onclick, options = {}) { + const { isVisible = false, ...restOptions } = options; + const mergedOptions = { + class: `chevron-button chevron-${isVisible ? 'right' : 'left'}`, + ...restOptions, + }; + + return h('a.btn', { + ...mergedOptions, + onclick, + }, isVisible ? iconChevronRight() : iconChevronLeft()); +} diff --git a/QualityControl/public/common/constants/drawingOptions.js b/QualityControl/public/common/constants/drawingOptions.js new file mode 100644 index 000000000..623be5ee2 --- /dev/null +++ b/QualityControl/public/common/constants/drawingOptions.js @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction.p + */ + +const DRAW_OPTIONS = ['lego', 'colz', 'lcolz', 'text']; +const DISPLAY_HINTS = ['logx', 'logy', 'logz', 'gridx', 'gridy', 'gridz', 'stat']; + +export { DRAW_OPTIONS, DISPLAY_HINTS }; + +export const DRAWING_OPTIONS = new Set([...DRAW_OPTIONS, ...DISPLAY_HINTS]); diff --git a/QualityControl/public/common/object/objectDrawingOptions.js b/QualityControl/public/common/object/objectDrawingOptions.js new file mode 100644 index 000000000..0e64ddda3 --- /dev/null +++ b/QualityControl/public/common/object/objectDrawingOptions.js @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction.p + */ + +import { h } from '/js/src/index.js'; +import { DRAW_OPTIONS } from '../constants/drawingOptions.js'; +import { DISPLAY_HINTS } from '../constants/drawingOptions.js'; + +/** + * Display options overlay for a QC object + * @param {object} options - The options object + * @param {string} options.id - The unique identifier for the object + * @param {boolean} options.ignoreDefaults - Whether to ignore default drawing options + * @param {Array} options.options - Array of selected draw options and display hints + * @param {Array} options.nonRecognizedDrawingOptions - Array of non-recognized drawing options + * @param {() => void} options.onToggleIgnoreDefaults - Callback to toggle ignore defaults + * @param {(option: string) => void} options.onToggleOption - Callback to toggle a drawing option or display hint + * @returns {vnode} Virtual DOM node representing the display options panel + */ +export const objectDrawingOptions = ({ + id, + ignoreDefaults, + options, + nonRecognizedDrawingOptions, + onToggleIgnoreDefaults, + onToggleOption, +}) => + h('.absolute-fill.level1.scroll-y.#objectDrawingOptions', [ + h('.absolute.right-0.top-0.bg-white.shadow-lg.w-100.h-100.overflow-auto', [ + h('.flex-row.items-center.justify-between.mb2.g2', [ + h('span', 'Drawing Options:'), + checkboxWithTooltip({ + id: `${id}ignoreDefaults`, + label: 'Ignore defaults', + tooltipText: 'Set by ROOT (fOption) and QC Metadata', + checked: ignoreDefaults, + onChange: onToggleIgnoreDefaults, + }), + ]), + nonRecognizedDrawingOptions && nonRecognizedDrawingOptions.length > 0 && + h('.flex-row.label.mv2.danger', `Non-recognized options: ${nonRecognizedDrawingOptions.join(', ')}`), + sectionTitle('Draw Options:', ' ROOT draw options'), + checkboxGrid(DRAW_OPTIONS.map((option) => + checkBox(id + option, option, options.includes(option), () => onToggleOption(option)))), + sectionTitle('Display Hints:', ' Canvas display hints'), + checkboxGrid(DISPLAY_HINTS.map((option) => + checkBox(id + option, option, options.includes(option), () => onToggleOption(option)))), + ]), + ]); + +const checkboxGrid = (children) => + h('.flex-column.g2', { + style: { + display: 'grid', + gap: '8px 12px', + gridTemplateColumns: 'repeat(auto-fit, minmax(115px, 115px))', + maxWidth: '400px', + }, + }, children); + +const sectionTitle = (label, tooltipText) => + h('.flex-row.mv2', h('.tooltip', [h('label.m0', label), h('.tooltiptext', tooltipText)])); + +const checkBox = (id, option, checked, onChange) => + h('.form-check', [ + h('input.form-check-input', { + type: 'checkbox', + id: id, + checked, + onchange: onChange, + }), + h('label.m0', { for: id }, option), + ]); + +const checkboxWithTooltip = ({ id, label, tooltipText, checked, onChange }) => + h('.form-check.tooltip.mt2-sm.mh2', [ + h('input.form-check-input', { + type: 'checkbox', + id: id, + checked, + onchange: onChange, + }), + h('label.m0', { for: id }, label), + h('span.tooltiptext', tooltipText), + ]); diff --git a/QualityControl/public/common/object/objectInfoCard.js b/QualityControl/public/common/object/objectInfoCard.js index 184bc681f..83c338bf6 100644 --- a/QualityControl/public/common/object/objectInfoCard.js +++ b/QualityControl/public/common/object/objectInfoCard.js @@ -14,6 +14,7 @@ import { h } from '/js/src/index.js'; import { camelToTitleCase, copyToClipboard, isContextSecure, prettyFormatDate } from './../utils.js'; +import { visibilityButton } from '../visibilityButton.js'; const SPECIFIC_KEY_LABELS = { id: 'ID (etag)', @@ -22,6 +23,8 @@ const SPECIFIC_KEY_LABELS = { const DATE_FIELDS = ['validFrom', 'validUntil', 'createdAt', 'lastModified']; const TO_REMOVE_FIELDS = ['etag', 'qcObject', 'versions', 'name', 'location']; const HIGHLIGHTED_FIELDS = ['runNumber', 'runType', 'path', 'qcVersion']; +const DRAW_OPTIONS_FIELD = 'drawOptions'; +const DISPLAY_HINTS_FIELD = 'displayHints'; const KEY_TO_RENDER_FIRST = 'path'; @@ -32,23 +35,95 @@ const KEY_TO_RENDER_FIRST = 'path'; * @param {function(Notification): function(string, string): object} rowAttributes - * An optional curried function that returns the VNode attribute builder. * Use {@link defaultRowAttributes} exported from this module, supplying the Notification API. + * @param {() => void} onToggleDrawingOptions - Callback function to for displaying drawing options * @returns {vnode} - panel with information about the object * @example * ``` * qcObjectInfoPanel(qcObject, {}, defaultRowAttributes(model.notification)) + * qcObjectInfoPanel( + * qcObject, + * { gap: '.5em' }, + * defaultRowAttributes(model.notification), + * () => objectViewModel.toggleDrawingOptionsVisible(), + * ) * ``` */ -export const qcObjectInfoPanel = (qcObject, style = {}, rowAttributes = () => undefined) => +export const qcObjectInfoPanel = ( + qcObject, + style = {}, + rowAttributes = () => undefined, + onToggleDrawingOptions = null, // Default to null to indicate it's optional +) => h('.flex-column.scroll-y#qcObjectInfoPanel', { style }, [ [ KEY_TO_RENDER_FIRST, ...Object.keys(qcObject) - .filter((key) => - key !== KEY_TO_RENDER_FIRST && !TO_REMOVE_FIELDS.includes(key)), + .filter((k) => k !== KEY_TO_RENDER_FIRST && !TO_REMOVE_FIELDS.includes(k)), ] - .map((key) => infoRow(key, qcObject[key], rowAttributes)), + .flatMap((key) => { + if (key === DRAW_OPTIONS_FIELD && typeof onToggleDrawingOptions === 'function') { + return [ + groupedInfoRow({ + keyValuePair1: { key: DRAW_OPTIONS_FIELD, value: qcObject.drawOptions }, + keyValuePair2: { key: DISPLAY_HINTS_FIELD, value: qcObject.displayHints }, + infoRowAttributes: defaultRowAttributes, + buttonElement: visibilityButton(onToggleDrawingOptions, { title: 'Toggle drawing options' }), + }), + ]; + } + if (key === DISPLAY_HINTS_FIELD && typeof onToggleDrawingOptions === 'function') { + return []; // Hide displayHints (it is shown inside the grouped info row) + } + return [infoRow(key, qcObject[key], rowAttributes)]; + }), ]); +/** + * Builds two info rows grouped together with an action button on the side + * @param {object} params - parameters object + * @param {object} params.keyValuePair1 - first key value pair + * @param {object} params.keyValuePair2 - second key value pair + * @param {function(string, string): object} params.infoRowAttributes - function that return given attributes + * for the row + * @param {vnode} params.buttonElement - button element to be displayed on the side + * @returns {vnode} - grouped info row with action button + */ +const groupedInfoRow = ({ keyValuePair1, keyValuePair2, infoRowAttributes, buttonElement }) => { + const { key: key1, value: value1 } = keyValuePair1; + const { key: key2, value: value2 } = keyValuePair2; + const highlightedClassesKey1 = HIGHLIGHTED_FIELDS.includes(key1) ? '.highlighted' : ''; + const highlightedClassesKey2 = HIGHLIGHTED_FIELDS.includes(key2) ? '.highlighted' : ''; + const formattedValue1 = infoPretty(key1, value1); + const formattedKey1 = getUILabel(key1); + const hasValue1 = value1 != null && value1 !== '' && (!Array.isArray(value1) || value1.length !== 0); + const formattedValue2 = infoPretty(key2, value2); + const formattedKey2 = getUILabel(key2); + const hasValue2 = value2 != null && value2 !== '' && (!Array.isArray(value2) || value2.length !== 0); + return h('.flex-row.relative', [ + h('.flex-column.w-100.g2', [ + h(`.flex-row.g2.info-row${highlightedClassesKey1}`, [ + h('b.w-25.w-wrapped', formattedKey1), + h('.w-75', { style: 'padding-right: 50px', // avoid overlap with button + ...hasValue1 + ? infoRowAttributes(formattedKey1, formattedValue1) + : {} }, formattedValue1), + ]), + h(`.flex-row.g2.info-row${highlightedClassesKey2}`, [ + h('b.w-25.w-wrapped', formattedKey2), + h('.w-75', { style: 'padding-right: 50px', // avoid overlap with button + ...hasValue2 + ? infoRowAttributes(formattedKey2, formattedValue2) + : {} }, formattedValue2), + ]), + ]), + h( + '.absolute.items-center.level3', + { style: 'top:50%;right:0;transform:translateY(-50%)' }, + buttonElement, + ), + ]); +}; + /** * Builds a raw with the key and value information parsed based on their type * @param {string} key - key of the object info @@ -60,12 +135,14 @@ const infoRow = (key, value, infoRowAttributes) => { const highlightedClasses = HIGHLIGHTED_FIELDS.includes(key) ? '.highlighted' : ''; const formattedValue = infoPretty(key, value); const formattedKey = getUILabel(key); - const hasValue = value != null && value !== '' && (!Array.isArray(value) || value.length !== 0); - return h(`.flex-row.g2.info-row${highlightedClasses}`, [ h('b.w-25.w-wrapped', formattedKey), - h('.w-75.cursor-pointer', hasValue && infoRowAttributes(formattedKey, formattedValue), formattedValue), + h( + `.w-75 ${hasValue ? 'cursor-pointer' : 'cursor-none'}`, + hasValue ? infoRowAttributes(formattedKey, formattedValue) : {}, + formattedValue, + ), ]); }; diff --git a/QualityControl/public/common/visibilityButton.js b/QualityControl/public/common/visibilityButton.js index e38f25ba1..0f9937bda 100644 --- a/QualityControl/public/common/visibilityButton.js +++ b/QualityControl/public/common/visibilityButton.js @@ -12,42 +12,26 @@ * or submit itself to any jurisdiction. */ -import { h, iconChevronLeft, iconChevronRight } from '/js/src/index.js'; +import { h, iconEye } from '/js/src/index.js'; /** - * @typedef {object} VisibilityToggleButtonOptions + * @typedef {object} VisibilityButtonOptions * @property {string} [id] - html id property. * @property {string} [class] - html class property, multiple classes can be passed as a single space seperated string. * @property {string} [title] - title shown on hover. - * @property {boolean} [isVisible=true] - determines which icon is rendered. */ /** - * Visibility toggle button. - * Creates an anchor element that displays an **eye** icon if visible or a **closed eye / no-eye** icon if hidden. - * @param {VisibilityToggleButtonOptions} options - Virtual node options. + * Creates a visibility toggle button with an eye icon * @param {() => void} onclick - Callback invoked when the button is clicked. - * @returns {vnode} - Visibility toggle button vnode. - * @example - * visibilityToggleButton( - * { - * isVisible: objectViewModel.getObjectInfoVisible(), - * title: 'Toggle object information visibility', - * }, - * () => { - * objectViewModel.toggleObjectInfoVisible(); - * }, - * ); + * @param {VisibilityButtonOptions} options - Additional options for the button element + * @returns {object} - Visibility toggle button vnode. */ -export function visibilityToggleButton(options = {}, onclick) { - const { isVisible = true, ...restOptions } = options; - const mergedOptions = { - class: `visibility-toggle-button visibility-toggle-${isVisible ? 'on' : 'off'}`, - ...restOptions, - }; - +export function visibilityButton(onclick, options = {}) { + const { ...rest } = options; return h('a.btn', { - ...mergedOptions, + ...rest, + class: ['visibility-toggle-button', rest.class || ''].join(' ').trim(), onclick, - }, isVisible ? iconChevronRight() : iconChevronLeft()); + }, iconEye()); } diff --git a/QualityControl/public/pages/objectView/ObjectViewModel.js b/QualityControl/public/pages/objectView/ObjectViewModel.js index 0063f7fc8..11c209b13 100644 --- a/QualityControl/public/pages/objectView/ObjectViewModel.js +++ b/QualityControl/public/pages/objectView/ObjectViewModel.js @@ -16,6 +16,7 @@ import { BaseViewModel } from '../../common/abstracts/BaseViewModel.js'; import { setBrowserTabTitle } from '../../common/utils.js'; import { RemoteData, BrowserStorage } from '/js/src/index.js'; import { StorageKeysEnum } from '../../common/enums/storageKeys.enum.js'; +import { DRAWING_OPTIONS } from '../../common/constants/drawingOptions.js'; /** * Model namespace for ObjectViewPage @@ -41,10 +42,21 @@ export default class ObjectViewModel extends BaseViewModel { */ this.selected = RemoteData.notAsked(); - this.drawingOptions = []; - this.displayHints = []; - this.ignoreDefaults = false; + /** + * Options for previewing object drawing options. + */ + this.ignoreDefaults = false; // whether to ignore default (options from object) drawing options + this.layoutDisplayOptions = []; // options from layout tab object if opened from layout + this.drawingOptions = []; // active drawing options for previewing + this.drawOptions = []; // options from object + this.displayHints = []; // options from object + this.nonRecognizedDrawingOptions = []; // options on drawingOptions or layoutDisplayOptions that are not recognized + this.objectDrawingOptionsVisible = false; + /** + * Tracks whether the object information panel is currently visible. + */ + this._objectInfoVisible = true; this._storage = new BrowserStorage(StorageKeysEnum.OBJECT_VIEW_INFO_VISIBILITY_SETTING); this._loadObjectInfoVisible(); } @@ -75,7 +87,10 @@ export default class ObjectViewModel extends BaseViewModel { async updateObjectSelection(object, validFrom = undefined, id = '') { const { objectName = undefined, objectId = undefined } = object; const { params } = this.model.router; - const context = { objectName: objectName || params.objectName, objectId: objectId || params.objectId }; + const context = { + objectName: objectName || params.objectName, + objectId: objectId || params.objectId, + }; const { refreshNeeded, data } = await this.model.object.checkIfRefreshObject(this.selected.payload, context); if (!refreshNeeded) { return; @@ -100,6 +115,7 @@ export default class ObjectViewModel extends BaseViewModel { this.selected = await this.model.services.object.getObjectById(params.objectId, id, validFrom, this); } + this._initialDrawingOptions(); setBrowserTabTitle(this.selected.payload.name); Object.entries(params).forEach(([key, value]) => { @@ -119,6 +135,57 @@ export default class ObjectViewModel extends BaseViewModel { this.notify(); } + /** + * Set the initial drawing options based on the selected object + */ + _initialDrawingOptions() { + const { + ignoreDefaults = false, + drawOptions = [], + displayHints = [], + layoutDisplayOptions = [], + } = this.selected.payload; + this.ignoreDefaults = Boolean(ignoreDefaults); + this.drawOptions = drawOptions; + this.displayHints = displayHints; + this.layoutDisplayOptions = layoutDisplayOptions; + + this.drawingOptions = ignoreDefaults + ? [...layoutDisplayOptions] + : [...drawOptions, ...displayHints, ...layoutDisplayOptions]; + + const all = new Set([...drawOptions, ...displayHints, ...layoutDisplayOptions]); + this.nonRecognizedDrawingOptions = [...all].filter((o) => !DRAWING_OPTIONS.has(o)); + this.notify(); + } + + /** + * Toggle the ignoreDefaults for drawing options. + * When ignored, default drawing options on object will not be applied. + */ + toggleIgnoreDefaults() { + this.ignoreDefaults = !this.ignoreDefaults; + this.drawingOptions = this.ignoreDefaults + ? [...this.layoutDisplayOptions] + : [...this.drawOptions, ...this.displayHints, ...this.layoutDisplayOptions]; + this.notify(); + } + + /** + * Toggle a drawing option on or off. + * If the option is currently enabled, it will be disabled; if disabled, it will be enabled. + * @param {string} option - the drawing option to toggle + */ + toggleDrawingOption(option) { + const index = this.drawingOptions.indexOf(option); + if (index >= 0) { + this.drawingOptions.splice(index, 1); + } else { + this.drawingOptions.push(option); + } + this.notify(); + } + /** * Creates the href url for the download element * @param {string} objectId - id of root object @@ -165,6 +232,15 @@ export default class ObjectViewModel extends BaseViewModel { this.notify(); } + /** + * Toggle the display state of object drawing options panel. + * If currently visible, it becomes hidden; if hidden, it becomes visible. + */ + toggleDrawingOptionsVisible() { + this.objectDrawingOptionsVisible = !this.objectDrawingOptionsVisible; + this.notify(); + } + /** * Sets routing parameters for object retrieval based on the available information. * Ensures only one of objectName or objectId is set, and removes irrelevant keys. diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index b296c716c..92f7b1a36 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -19,7 +19,8 @@ import { errorDiv } from '../../common/errorDiv.js'; import { dateSelector } from '../../common/object/dateSelector.js'; import { defaultRowAttributes, qcObjectInfoPanel } from '../../common/object/objectInfoCard.js'; import { downloadButton } from '../../common/downloadButton.js'; -import { visibilityToggleButton } from '../../common/visibilityButton.js'; +import { chevronButton } from '../../common/chevronButton.js'; +import { objectDrawingOptions } from '../../common/object/objectDrawingOptions.js'; /** * Shows a page to view an object on the whole page @@ -41,19 +42,14 @@ const objectPlotAndInfo = (objectViewModel) => Loading: () => spinner(10, 'Loading object...'), Failure: (error) => errorDiv(error), Success: (qcObject) => { + const { id, validFrom, versions } = qcObject; const { - id, - validFrom, - ignoreDefaults = false, - drawOptions = [], - displayHints = [], - layoutDisplayOptions = [], - versions, - } = qcObject; - const drawingOptions = ignoreDefaults ? - layoutDisplayOptions - : [...drawOptions, ...displayHints, ...layoutDisplayOptions]; - const isObjectInfoVisible = objectViewModel.objectInfoVisible; + ignoreDefaults, + drawingOptions, + nonRecognizedDrawingOptions, + objectInfoVisible, + objectDrawingOptionsVisible, + } = objectViewModel; return h('.w-100.h-100.flex-column.scroll-off#ObjectPlot', [ h('.flex-row.justify-center.items-center.h-10', [ h( @@ -69,28 +65,44 @@ const objectPlotAndInfo = (objectViewModel) => href: objectViewModel.getDownloadQcdbObjectUrl(qcObject.id), title: 'Download object', }), - visibilityToggleButton( - { - isVisible: isObjectInfoVisible, - title: 'Toggle object information visibility', - }, + chevronButton( () => objectViewModel.toggleObjectInfoVisible(), + { isVisible: objectInfoVisible, title: 'Toggle object information visibility' }, ), ]), ]), - h('.w-100.flex-row.g2.m2', { style: 'height: 0;flex-grow:1' }, [ + h('.flex-row.g2.m2.flex-grow', [ h('.flex-grow', { - // Key change forces redraw when toggling info panel - key: isObjectInfoVisible ? 'objectPlotWithoutInfoPanel' : 'objectPlotWithInfoPanel', + // force redraw on toggle info panel and update drawing options + key: `${objectInfoVisible}-${drawingOptions}`, }, drawObject(qcObject, {}, drawingOptions, (error) => { objectViewModel.drawingFailureOccurred(error.message); })), - isObjectInfoVisible && h('.scroll-y.w-30', { - key: 'objectInfoPanel', - }, [ - h('h3.text-center', 'Object information'), - qcObjectInfoPanel(qcObject, { gap: '.5em' }, defaultRowAttributes(model.notification)), - ]), + objectInfoVisible && + h('.scroll-y.w-30.relative', { + key: 'objectInfoPanel', // force redraw on toggle drawing options panel + }, [ + objectDrawingOptionsVisible && + objectDrawingOptions({ + id, + ignoreDefaults: ignoreDefaults, + options: drawingOptions, + nonRecognizedDrawingOptions: nonRecognizedDrawingOptions, + onToggleIgnoreDefaults: () => objectViewModel.toggleIgnoreDefaults(), + onToggleOption: (option) => objectViewModel.toggleDrawingOption(option), + }), + h('', [ + h('h3.text-center', 'Object information'), + h('', [ + qcObjectInfoPanel( + qcObject, + { gap: '.5em' }, + defaultRowAttributes(model.notification), + () => objectViewModel.toggleDrawingOptionsVisible(), + ), + ]), + ]), + ]), ]), ]); }, diff --git a/QualityControl/test/api/layouts/api-get-layout.test.js b/QualityControl/test/api/layouts/api-get-layout.test.js index 068b338bb..c74a2e3dd 100644 --- a/QualityControl/test/api/layouts/api-get-layout.test.js +++ b/QualityControl/test/api/layouts/api-get-layout.test.js @@ -16,7 +16,7 @@ import { suite, test } from 'node:test'; import { OWNER_TEST_TOKEN, URL_ADDRESS } from '../config.js'; import request from 'supertest'; import { deepStrictEqual } from 'node:assert'; -import { LAYOUT_MOCK_4, LAYOUT_MOCK_5, LAYOUT_MOCK_6 } from '../../demoData/layout/layout.mock.js'; +import { LAYOUT_MOCK_4, LAYOUT_MOCK_5, LAYOUT_MOCK_6, LAYOUT_MOCK_7 } from '../../demoData/layout/layout.mock.js'; export const apiGetLayoutsTests = () => { suite('GET /layouts', () => { @@ -44,7 +44,11 @@ export const apiGetLayoutsTests = () => { throw new Error('Expected array of layouts'); } - deepStrictEqual(res.body, [LAYOUT_MOCK_4, LAYOUT_MOCK_5], 'Unexpected Layout structure was returned'); + deepStrictEqual( + res.body, + [LAYOUT_MOCK_4, LAYOUT_MOCK_5, LAYOUT_MOCK_7], + 'Unexpected Layout structure was returned', + ); }); }); diff --git a/QualityControl/test/demoData/layout/layout.mock.js b/QualityControl/test/demoData/layout/layout.mock.js index 041c01a3e..f93e39975 100644 --- a/QualityControl/test/demoData/layout/layout.mock.js +++ b/QualityControl/test/demoData/layout/layout.mock.js @@ -269,3 +269,34 @@ export const LAYOUT_MOCK_6 = { ], collaborators: [], }; + +export const LAYOUT_MOCK_7 = { + id: 'q12b8c22402408122e2f20dd', + name: 'drawing-test', + owner_id: 0, + owner_name: 'Anonymous', + description: '', + displayTimestamp: false, + autoTabChange: 0, + tabs: [ + { + id: 'b12b8c227b3227b0c603c29d', + name: 'main', + objects: [ + { + id: 'b12b8c25d5b49dbf80e81926', + x: 0, + y: 0, + h: 1, + w: 1, + name: 'qc/test/object/12', + options: ['logx', 'text'], + autoSize: false, + ignoreDefaults: true, + }, + ], + columns: 1, + }, + ], + collaborators: [], +}; diff --git a/QualityControl/test/public/features/filterTest.test.js b/QualityControl/test/public/features/filterTest.test.js index bb4130d09..fff4a7a4c 100644 --- a/QualityControl/test/public/features/filterTest.test.js +++ b/QualityControl/test/public/features/filterTest.test.js @@ -63,7 +63,7 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { await page.locator('#clearFilterButton').click(); await delay(100); objectList = await page.evaluate(() => window.model.object.list); - strictEqual(objectList.length, 3); + strictEqual(objectList.length, 4); }); await testParent.test('ObjectShow should only list versions based on the filter', { timeout }, async () => { @@ -91,7 +91,7 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { await extendTree(3, 5); let rowCount = await page.evaluate(() => document.querySelectorAll('tr').length); - strictEqual(rowCount, 7); + strictEqual(rowCount, 8); const runNumber = '0'; await page.locator('#runNumberFilter').fill(runNumber); diff --git a/QualityControl/test/public/pages/layout-list.test.js b/QualityControl/test/public/pages/layout-list.test.js index 8308a6013..7bc7bb0a8 100644 --- a/QualityControl/test/public/pages/layout-list.test.js +++ b/QualityControl/test/public/pages/layout-list.test.js @@ -120,7 +120,7 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent) }); await testParent.test('should have a link to show a layout from users layout', async () => { - const linkpath = cardLayoutLinkPath(cardPath(myLayoutIndex, 2)); + const linkpath = cardLayoutLinkPath(cardPath(myLayoutIndex, 3)); const href = await page.evaluate((path) => document.querySelector(path).href, linkpath); strictEqual(href, 'http://localhost:8080/?page=layoutShow&layoutId=671b8c22402408122e2f20dd'); @@ -188,7 +188,7 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent) await testParent.test('should have a folder with one card after object path filtering', async () => { const preFilterCardCount = await page.evaluate(() => document.querySelectorAll('.card').length); - strictEqual(preFilterCardCount, 2); + strictEqual(preFilterCardCount, 3); await page.locator('#openFilterToggle').click(); await delay(100); await page.locator(filterObjectPath).fill('qc/MCH/QO/'); @@ -220,14 +220,14 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent) await page.locator('div.m2:nth-child(3) > div:nth-child(1)').click(); await delay(100); const preFilterCardCount = await page.evaluate(() => document.querySelectorAll('.card').length); - strictEqual(preFilterCardCount, 5); + strictEqual(preFilterCardCount, 7); await page.locator('#openFilterToggle').click(); await delay(100); await page.locator(filterObjectPath).fill('object'); await page.locator('#openFilterToggle').click(); await delay(100); let postFilterCardCount = await page.evaluate(() => document.querySelectorAll('.card').length); - strictEqual(postFilterCardCount, 3); + strictEqual(postFilterCardCount, 5); await page.locator(filterPath).fill('pdpBeamType'); await delay(100); postFilterCardCount = await page.evaluate(() => document.querySelectorAll('.card').length); @@ -239,8 +239,8 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent) await page.goto(`${url}${LAYOUT_LIST_PAGE_PARAM}`, { waitUntil: 'networkidle0' }); await delay(100); const preFilterCardCount = await page.evaluate(() => document.querySelectorAll('.card').length); - strictEqual(preFilterCardCount, 2); - await page.locator(filterPath).fill('a'); + strictEqual(preFilterCardCount, 3); + await page.locator(filterPath).fill('a-test'); await delay(100); const postFilterCardCount = await page.evaluate(() => document.querySelectorAll('.card').length); diff --git a/QualityControl/test/public/pages/object-view-from-layout-show.test.js b/QualityControl/test/public/pages/object-view-from-layout-show.test.js index 07b65f280..05332725c 100644 --- a/QualityControl/test/public/pages/object-view-from-layout-show.test.js +++ b/QualityControl/test/public/pages/object-view-from-layout-show.test.js @@ -145,52 +145,52 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t ); await testParent.test( - 'should have a correctly made object info visibility button', + 'should have a correctly made object info chevron button', { timeout }, async () => { - const visibilityButtonClass = await page.evaluate(() => - document.querySelector('.visibility-toggle-button').className); - match(visibilityButtonClass, /visibility-toggle-(on|off)/i); + const chevronButtonClass = await page.evaluate(() => + document.querySelector('.chevron-button').className); + match(chevronButtonClass, /chevron-(right|left)/i); }, ); await testParent.test( - 'should have download button and visibility button inline and to the right of the timestamp dropdown', + 'should have download button and chevron button inline and to the right of the timestamp dropdown', { timeout }, async () => { const positions = await page.evaluate(() => { const dateSelector = document.querySelector('#dateSelector'); const dlButton = document.querySelector('.download-button'); - const visibilityButton = document.querySelector('.visibility-toggle-button'); + const chevronButton = document.querySelector('.chevron-button'); - if (!dateSelector || !dlButton || !visibilityButton) { + if (!dateSelector || !dlButton || !chevronButton) { throw new Error('One or more elements not found on the page'); } const dateRect = dateSelector.getBoundingClientRect(); const dlRect = dlButton.getBoundingClientRect(); - const visRect = visibilityButton.getBoundingClientRect(); + const chevronRect = chevronButton.getBoundingClientRect(); // Helper to get vertical center const verticalCenter = (rect) => (rect.top + rect.bottom) / 2; const dateCenter = verticalCenter(dateRect); const dlCenter = verticalCenter(dlRect); - const visCenter = verticalCenter(visRect); + const chevronCenter = verticalCenter(chevronRect); return { dlRightOfDate: dlRect.left > dateRect.right, - visRightOfDate: visRect.left > dateRect.right, - sameY: Math.abs(dateCenter - dlCenter) < 1 && Math.abs(dateCenter - visCenter) < 1, + chevronRightOfDate: chevronRect.left > dateRect.right, + sameY: Math.abs(dateCenter - dlCenter) < 1 && Math.abs(dateCenter - chevronCenter) < 1, }; }); strictEqual(positions.dlRightOfDate, true, 'Download button is not to the right of the timestamp dropdown'); - strictEqual(positions.visRightOfDate, true, 'Visibility button is not to the right of the timestamp dropdown'); + strictEqual(positions.chevronRightOfDate, true, 'Chevron button is not to the right of the timestamp dropdown'); strictEqual( positions.sameY, true, - 'Download button, visibility button, and timestamp dropdown are not vertically aligned within 1px', + 'Download button, chevron button, and timestamp dropdown are not vertically aligned within 1px', ); }, ); @@ -322,19 +322,19 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t ); await testParent.test( - 'should toggle the object information panel visibility when the visibility (toggle) button is clicked', + 'should toggle the object information panel visibility when the chevron button is clicked', { timeout }, async () => { // Capture initial visibility state const initialVisibility = await getObjectInfoPanelVisibility(page); // Click the toggle button once and check visibility - await page.click('.visibility-toggle-button'); + await page.click('.chevron-button'); await delay(100); const afterFirstClick = await getObjectInfoPanelVisibility(page); // Click the toggle button again to restore original state - await page.click('.visibility-toggle-button'); + await page.click('.chevron-button'); await delay(100); const afterSecondClick = await getObjectInfoPanelVisibility(page); @@ -352,7 +352,7 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t ); await testParent.test( - 'should redraw the JSRoot object drawing when visibility toggle changes', + 'should redraw the JSRoot object drawing when object info panel visibility changes', { timeout }, async () => { const initialElement = await page.waitForSelector( @@ -360,7 +360,7 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t { timeout: 1000 }, ); - await page.click('.visibility-toggle-button'); + await page.click('.chevron-button'); await delay(100); const newElement = await page.waitForSelector( @@ -370,12 +370,12 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t const redrawn = await page.evaluate((a, b) => a.innerHTML !== b.innerHTML, initialElement, newElement); - strictEqual(redrawn, true, 'JSRoot drawing was not redrawn on visibility toggle'); + strictEqual(redrawn, true, 'JSRoot drawing was not redrawn on object info panel visibility change'); }, ); await testParent.test( - 'should update localStorage state when visibility toggle button is clicked', + 'should update localStorage state when chevron button is clicked', { timeout }, async () => { const personId = await page.evaluate(() => window.model?.session?.personid?.toString()); @@ -387,12 +387,12 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t const visibilitySettingInitially = await getLocalStorageAsJson(page, localStorageKey); // Click the toggle button (first time) - await page.click('.visibility-toggle-button'); + await page.click('.chevron-button'); await delay(100); const visibilitySettingAfterFirstClick = await getLocalStorageAsJson(page, localStorageKey); // Click the toggle button (second time) - await page.click('.visibility-toggle-button'); + await page.click('.chevron-button'); await delay(100); const visibilitySettingAfterSecondClick = await getLocalStorageAsJson(page, localStorageKey); @@ -409,6 +409,154 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t }, ); + await testParent.test( + 'should set drawing options from layout when loading an object from a layout', + { timeout }, + async () => { + const layoutId = 'q12b8c22402408122e2f20dd'; + const objectId = 'b12b8c25d5b49dbf80e81926'; + const expectedDrawingOptions = ['logx', 'text']; + const expectedIgnoreDefaults = true; + await page.goto( + `${url}?page=objectView&objectId=${objectId}&layoutId=${layoutId}`, + { waitUntil: 'networkidle0' }, + ); + // Click the toggle button to show object details panel containing drawing options visibility button + await page.click('.chevron-button'); + await delay(100); + + const result = await page.evaluate(() => { + const { ignoreDefaults } = model.objectViewModel; + const { drawingOptions } = model.objectViewModel; + const plotElement = document.querySelector('#ObjectPlot > div:nth-child(2) > div:nth-child(1) > div'); + const fingerprint = plotElement.dataset.fingerprintData; + return { fingerprint, drawingOptions, ignoreDefaults }; + }); + + const ignoreDefaultsMatches = result.ignoreDefaults === expectedIgnoreDefaults; + const allOptionsPresent = expectedDrawingOptions.every((option) => result.fingerprint.includes(option)); + strictEqual(ignoreDefaultsMatches, true, 'ignoreDefaults does not match expected value from layout'); + strictEqual(allOptionsPresent, true, 'Not all expected drawing options are present in the plot fingerprint'); + }, + ); + + await testParent.test( + 'should initially hide drawing options panel', + { timeout }, + async () => { + const exists = await page.evaluate(() => Boolean(document.querySelector('#objectDrawingOptions'))); + strictEqual(exists, false, 'Drawing options panel should be initially hidden'); + }, + ); + + await testParent.test( + 'should show drawing options panel on click visibility toggle button', + { timeout }, + async () => { + await page.click('.visibility-toggle-button'); + await delay(200); + const exists = await page.evaluate(() => Boolean(document.querySelector('#objectDrawingOptions'))); + strictEqual(exists, true, 'Drawing options panel should be visible after click visibility button'); + }, + ); + + await testParent.test( + 'should display checked ignore defaults button', + { timeout }, + async () => { + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const ignoreDefaultsCheckboxSelector = `#objectDrawingOptions input[id="${objectId}ignoreDefaults"]`; + const isChecked = await page.evaluate((selector) => { + const checkbox = document.querySelector(selector); + return checkbox ? checkbox.checked : null; + }, ignoreDefaultsCheckboxSelector); + strictEqual(isChecked, true, 'Ignore defaults checkbox should be checked based on layout setting'); + }, + ); + + await testParent.test( + 'should set active checkboxes from layout display options when ignore defaults is true', + { timeout }, + async () => { + const activeCheckboxLabels = await page.evaluate(() => { + const checkboxes = document.querySelectorAll('#objectDrawingOptions > div .flex-column input[type="checkbox"]'); + return Array.from(checkboxes) + .filter((checkbox) => checkbox.checked) + .map((checkbox) => document.querySelector(`#objectDrawingOptions label[for="${checkbox.id}"]`)?.innerText); + }); + const expectedDrawingOptions = ['logx', 'text']; + deepStrictEqual( + activeCheckboxLabels.sort(), + expectedDrawingOptions.sort(), + 'Active drawing options do not match expected layout settings', + ); + }, + ); + + await testParent.test( + 'should set active checkboxes from layout display options when ignore defaults is set to false', + { timeout }, + async () => { + await page.click('#objectDrawingOptions input[id="baffe0b2-826c-11ef-8f19-c0a80209250cignoreDefaults"]'); + const activeCheckboxLabels = await page.evaluate(() => { + const checkboxes = document.querySelectorAll('#objectDrawingOptions > div .flex-column input[type="checkbox"]'); + return Array.from(checkboxes) + .filter((checkbox) => checkbox.checked) + .map((checkbox) => document.querySelector(`#objectDrawingOptions label[for="${checkbox.id}"]`)?.innerText); + }); + const expectedDrawingOptions = ['logx', 'text']; + deepStrictEqual( + activeCheckboxLabels.sort(), + expectedDrawingOptions.sort(), + 'Active drawing options do not match expected layout settings', + ); + }, + ); + + await testParent.test( + 'should updated fingerprint on plot after changing drawing options', + { timeout }, + async () => { + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const logxCheckboxSelector = `#objectDrawingOptions input[id="${objectId}logx"]`; + + // Uncheck 'logx' option + await page.click(logxCheckboxSelector); + await delay(100); + + const fingerprintIncludesLogx = await page.evaluate(() => { + const plotElement = document.querySelector('#ObjectPlot > div:nth-child(2) > div:nth-child(1) > div'); + const fingerprint = plotElement.dataset.fingerprintData; + return fingerprint.includes('logx'); + }); + + strictEqual( + fingerprintIncludesLogx, + false, + 'Plot fingerprint should not include "logx" after unchecking the option', + ); + }, + ); + + await testParent.test( + 'should display unrecognized drawing options in drawing options panel', + { timeout }, + async () => { + const nonRecognizedDrawingOptionSelector = '#objectDrawingOptions > div > div:nth-child(2'; + const nonRecognizedDrawingOption = 'hist'; + const nonRecognizedOptionsText = await page.evaluate((selector) => { + const element = document.querySelector(selector); + return element ? element.textContent : ''; + }, nonRecognizedDrawingOptionSelector); + + strictEqual( + nonRecognizedOptionsText.includes(nonRecognizedDrawingOption), + true, + 'Non-recognized drawing options should be displayed in the drawing options panel', + ); + }, + ); + await testParent.test( 'should display an error when the JSROOT object fails to fetch due to a network failure', { timeout }, diff --git a/QualityControl/test/public/pages/object-view-from-object-tree.test.js b/QualityControl/test/public/pages/object-view-from-object-tree.test.js index b9a70e26e..3c4fe475d 100644 --- a/QualityControl/test/public/pages/object-view-from-object-tree.test.js +++ b/QualityControl/test/public/pages/object-view-from-object-tree.test.js @@ -12,6 +12,8 @@ */ import { strictEqual } from 'node:assert'; +import { delay } from '../../testUtils/delay.js'; + const OBJECT_VIEW_PAGE_PARAM = '?page=objectView'; export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, testParent) => { @@ -86,6 +88,136 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t }, ); + await testParent.test( + 'should set initial drawing options on plot', + async () => { + const path = 'qc/test/object/12'; + const expectedDrawingOptions = ['hist', 'gridy', 'text']; + await page.goto(`${url}?page=objectView&objectName=${path}`, { waitUntil: 'networkidle0' }); + const result = await page.evaluate(() => { + const { drawingOptions } = globalThis.model.objectViewModel; + const plotElement = document.querySelector('#ObjectPlot > div:nth-child(2) > div:nth-child(1) > div'); + const fingerprint = plotElement.dataset.fingerprintData; + return { fingerprint, drawingOptions }; + }); + const allOptionsPresent = expectedDrawingOptions.every((option) => result.fingerprint.includes(option)); + strictEqual(allOptionsPresent, true, 'Not all expected drawing options are present in the plot fingerprint'); + }, + ); + + await testParent.test( + 'should initially hide drawing options panel', + { timeout }, + async () => { + const exists = await page.evaluate(() => Boolean(document.querySelector('#objectDrawingOptions'))); + strictEqual(exists, false, 'Drawing options panel should be initially hidden'); + }, + ); + + await testParent.test( + 'should show drawing options panel on click visibility toggle button', + { timeout }, + async () => { + await page.click('.visibility-toggle-button'); + await delay(100); + const exists = await page.evaluate(() => Boolean(document.querySelector('#objectDrawingOptions'))); + strictEqual(exists, true, 'Drawing options panel should be visible after click visibility button'); + }, + ); + + await testParent.test( + 'should display ignore defaults button', + { timeout }, + async () => { + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const checkboxId = `${objectId}ignoreDefaults`; + const ignoreDefaultsCheckboxSelector = `#objectDrawingOptions input[id="${checkboxId}"]`; + const element = await page.evaluate((selector) => + Boolean(document.querySelector(selector)), ignoreDefaultsCheckboxSelector); + strictEqual(element, true, 'Ignore Defaults checkbox not found'); + }, + ); + + await testParent.test( + 'should have initial drawing options checkboxes checked', + async () => { + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const gridyCheckboxSelector = `#objectDrawingOptions input[id="${objectId}gridy"]`; + const gridxCheckboxSelector = `#objectDrawingOptions input[id="${objectId}gridx"]`; + + const element = await page.evaluate((selector) => + Boolean(document.querySelector(selector)), gridyCheckboxSelector); + + const gridyChecked = await page.evaluate((selector) => + document.querySelector(selector).checked, gridyCheckboxSelector); + const gridxChecked = await page.evaluate((selector) => + document.querySelector(selector).checked, gridxCheckboxSelector); + + strictEqual(element, true, 'gridy checkbox not found'); + strictEqual(gridyChecked, true, 'gridy checkbox should be checked'); + strictEqual(gridxChecked, false, 'gridx checkbox should not be checked'); + }, + ); + + await testParent.test( + 'should have ignore defaults checkbox unchecked by default', + { timeout }, + async () => { + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const checkboxId = `${objectId}ignoreDefaults`; + const ignoreDefaultsCheckboxSelector = `#objectDrawingOptions input[id="${checkboxId}"]`; + const checked = await page.evaluate((selector) => + document.querySelector(selector).checked, ignoreDefaultsCheckboxSelector); + strictEqual(checked, false, 'Ignore Defaults checkbox should be unchecked by default'); + }, + ); + + await testParent.test( + 'should update plot when a drawing option is toggled', + { timeout }, + async () => { + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const gridXCheckboxSelector = `#objectDrawingOptions input[id="${objectId}gridx"]`; + + const initialResult = await page.evaluate(() => { + const plotElement = document.querySelector('#ObjectPlot > div:nth-child(2) > div:nth-child(1) > div'); + const fingerprint = plotElement.dataset.fingerprintData; + return { fingerprint }; + }); + + await page.click(gridXCheckboxSelector); + await delay(200); + + const afterToggleResult = await page.evaluate(() => { + const plotElement = document.querySelector('#ObjectPlot > div:nth-child(2) > div:nth-child(1) > div'); + const fingerprint = plotElement.dataset.fingerprintData; + return { fingerprint }; + }); + + strictEqual( + initialResult.fingerprint.includes('gridx'), + false, + 'plot fingerprint should not contain gridx initially', + ); + strictEqual( + afterToggleResult.fingerprint.includes('gridx'), + true, + 'plot fingerprint should contain gridx after toggle', + ); + }, + ); + + await testParent.test( + 'should hide the drawing options panel on second click visibility toggle button', + { timeout }, + async () => { + await page.click('.visibility-toggle-button'); + await delay(100); + const exists = await page.evaluate(() => Boolean(document.querySelector('#objectDrawingOptions'))); + strictEqual(exists, false, 'Drawing options panel should be hidden'); + }, + ); + await testParent .test('should have an info button with full path and last modified when clicked (plot success)', async () => { await page.goto( diff --git a/QualityControl/test/setup/seeders/ccdbObjects.js b/QualityControl/test/setup/seeders/ccdbObjects.js index 04066287a..db692bdd9 100644 --- a/QualityControl/test/setup/seeders/ccdbObjects.js +++ b/QualityControl/test/setup/seeders/ccdbObjects.js @@ -40,12 +40,21 @@ export const objects = [ [VALID_FROM]: new Date('2023-12-03').valueOf(), [VALID_UNTIL]: new Date('2023-12-04').valueOf(), }, + { + [PATH]: 'qc/test/object/12', + [LAST_MODIFIED]: new Date('2024-02-03').valueOf(), + [CREATED]: new Date('2024-02-03').valueOf(), + [ID]: '', + [VALID_FROM]: new Date('2024-02-03').valueOf(), + [VALID_UNTIL]: new Date('2024-02-04').valueOf(), + }, ]; export const subfolders = [ 'qc/test/object/1', 'qc/test/object/2', 'qc/test/object/11', + 'qc/test/object/12', ]; export const MOCK_OBJECT_BY_ID_RESULT = { @@ -87,6 +96,14 @@ export const OBJECT_VERSIONS = [ }, ]; +export const MOCK_OBJECT_3_VERSIONS = [ + { + validFrom: 1728058750070, + createdAt: 1728058897718, + id: 'baffe0b2-826c-11ef-8f19-c0a80209250c', + }, +]; + export const OBJECT_VERSIONS_FILTERED_BY_RUN_NUMBER = [ { createdAt: 1656072357533, @@ -117,10 +134,34 @@ export const OBJECT_BY_PATH_RESULT = { location: '/download/016fa8ac-f3b6-11ec-b9a9-c0a80209250c', }; +export const OBJECT_3_BY_PATH_RESULT = { + id: 'baffe0b2-826c-11ef-8f19-c0a80209250c', + path: 'qc/test/object/12', + name: 'qc/test/object/12', + validFrom: 1728058750070, + validUntil: 1728058895900, + createdAt: 1728058897718, + lastModified: 1728058897000, + drawOptions: [], + displayHints: 'hist', + etag: 'baffe0b2-826c-11ef-8f19-c0a80209250c', + runNumber: '551890', + runType: 'PHYSICS', + partName: 'send', + qcCheckName: undefined, + qcQuality: undefined, + qcDetectorName: 'MFT', + qcTaskName: 'MFTClusterTask', + qcVersion: '1.150.0', + objectType: 'o2::quality_control_modules::common::TH1Ratio', + location: '/download/baffe0b2-826c-11ef-8f19-c0a80209250c', +}; + export const TREE_API_OBJECTS = [ { name: 'qc/test/object/1' }, { name: 'qc/test/object/2' }, { name: 'qc/test/object/11' }, + { name: 'qc/test/object/12' }, ]; export const OBJECT_LATEST_FILTERED_BY_RUN_NUMBER = [ @@ -130,3 +171,11 @@ export const OBJECT_LATEST_FILTERED_BY_RUN_NUMBER = [ name: 'qc/test/object/1', }, ]; + +export const OBJECT_3_LATEST_FILTERED_BY_RUN_NUMBER = [ + { + [PATH]: 'qc/test/object/12', + createdAt: 1728058897718, + name: 'qc/test/object/12', + }, +]; diff --git a/QualityControl/test/setup/seeders/object-view/mock-object-view.js b/QualityControl/test/setup/seeders/object-view/mock-object-view.js index 5a810faac..3ec6f965c 100644 --- a/QualityControl/test/setup/seeders/object-view/mock-object-view.js +++ b/QualityControl/test/setup/seeders/object-view/mock-object-view.js @@ -18,6 +18,21 @@ export const MOCK_OBJECT_IDENTIFICATION_RESPONSE = { subfolders: [], }; +export const MOCK_OBJECT_12_IDENTIFICATION_RESPONSE = { + path: 'qc/test/object/12', + latest: true, + patternMatching: false, + objects: [ + { + [PATH]: 'qc/test/object/12', + [ID]: '"baffe0b2-826c-11ef-8f19-c0a80209250c"', + [VALID_FROM]: 1728058750070, + [VALID_UNTIL]: 1728058895900, + }, + ], + subfolders: [], +}; + export const MOCK_OBJECT_1_DETAILS_RESPONSE = { date: 'Tue, 29 Oct 2024 13:48:07 GMT', server: 'Apache', @@ -50,6 +65,42 @@ export const MOCK_OBJECT_1_DETAILS_RESPONSE = { connection: 'Keep-Alive', }; +export const MOCK_OBJECT_12_DETAILS_RESPONSE = { + date: 'Wed, 17 Dec 2025 21:34:40 GMT', + server: 'Apache', + 'access-control-allow-origin': '*', + 'access-control-allow-headers': 'range', + 'access-control-expose-headers': 'content-range,content-length,accept-ranges', + 'access-control-allow-methods': 'HEAD,GET', + 'cache-control': 'no-cache', + 'content-location': '/download/baffe0b2-826c-11ef-8f19-c0a80209250c', + 'valid-until': '1728058895900', + 'valid-from': '1728058750070', + created: '1728058897718', + etag: '"baffe0b2-826c-11ef-8f19-c0a80209250c"', + 'last-modified': 'Fri, 04 Oct 2024 16:21:37 GMT', + 'content-disposition': 'inline;filename="TObject_1728058897708.root"', + objecttype: 'o2::quality_control_modules::common::TH1Ratio', + qc_detector_name: 'MFT', + drawoptions: 'text', + displayhints: ['hist', 'gridy'], + runnumber: '551890', + adjustableeov: '1', + qc_task_name: 'MFTClusterTask', + runtype: 'PHYSICS', + qc_task_class: 'o2::quality_control_modules::mft::QcMFTClusterTask', + periodname: 'LHC24am', + qc_version: '1.150.0', + partname: 'send', + path: 'qc/MFT/MO/MFTClusterTask/ClusterRinLayer/mClusterRinLayer1', + 'accept-ranges': 'bytes', + 'content-md5': 'c0c9f6bd8783668f8e1e66f15d003809', + 'content-type': 'application/root', + 'content-length': '7168', + 'keep-alive': 'timeout=5, max=99', + connection: 'Keep-Alive', +}; + export const MOCK_OBJECT_VERSIONS_RESPONSE = { objects: [ { @@ -65,6 +116,16 @@ export const MOCK_OBJECT_VERSIONS_RESPONSE = { ], }; +export const MOCK_OBJECT_12_VERSIONS_RESPONSE = { + objects: [ + { + [VALID_FROM]: 1728058750070, + [CREATED]: 1728058897718, + [ID]: 'baffe0b2-826c-11ef-8f19-c0a80209250c', + }, + ], +}; + export const MOCK_OBJECT_VERSIONS_RESPONSE_RUN_NUMBER_FILTER = { objects: [ { diff --git a/QualityControl/test/setup/seeders/qcg-mock-data-template.json b/QualityControl/test/setup/seeders/qcg-mock-data-template.json index 1106e9a56..0963835ea 100644 --- a/QualityControl/test/setup/seeders/qcg-mock-data-template.json +++ b/QualityControl/test/setup/seeders/qcg-mock-data-template.json @@ -174,6 +174,36 @@ } ], "collaborators": [] + }, + { + "id": "q12b8c22402408122e2f20dd", + "name": "drawing-test", + "owner_id":0, + "owner_name": "Anonymous", + "description": "", + "displayTimestamp": false, + "autoTabChange": 0, + "tabs": [ + { + "id": "b12b8c227b3227b0c603c29d", + "name": "main", + "objects": [ + { + "id": "b12b8c25d5b49dbf80e81926", + "x": 0, + "y": 0, + "h": 1, + "w": 1, + "name": "qc/test/object/12", + "options": ["logx", "text"], + "autoSize": false, + "ignoreDefaults": true + } + ], + "columns": 1 + } + ], + "collaborators": [] } ], "users": [ diff --git a/QualityControl/test/setup/testSetupForCcdb.js b/QualityControl/test/setup/testSetupForCcdb.js index f3bc28eff..f1b7ad726 100644 --- a/QualityControl/test/setup/testSetupForCcdb.js +++ b/QualityControl/test/setup/testSetupForCcdb.js @@ -21,7 +21,9 @@ import { config } from './../config.js'; import { objects, subfolders } from './seeders/ccdbObjects.js'; import { MOCK_LATEST_OBJECT_FILTERED_BY_RUN_NUMBER, MOCK_OBJECT_1_DETAILS_RESPONSE, MOCK_OBJECT_IDENTIFICATION_RESPONSE, - MOCK_OBJECT_VERSIONS_RESPONSE, MOCK_OBJECT_VERSIONS_RESPONSE_RUN_NUMBER_FILTER } + MOCK_OBJECT_VERSIONS_RESPONSE, MOCK_OBJECT_VERSIONS_RESPONSE_RUN_NUMBER_FILTER, + MOCK_OBJECT_12_IDENTIFICATION_RESPONSE, MOCK_OBJECT_12_DETAILS_RESPONSE, MOCK_OBJECT_12_VERSIONS_RESPONSE, +} from './seeders/object-view/mock-object-view.js'; import { CCDB_MOCK_VERSION } from './seeders/ccdbVersion.js'; @@ -110,7 +112,10 @@ export const initializeNockForCcdb = () => { .reply(200, null, MOCK_OBJECT_1_DETAILS_RESPONSE) .head('/qc/test/object/1/1656072357492/1971432357492/016fa8ac-f3b6-11ec-b9a9-c0a80209250c/RunNumber=0') - .reply(200, null, MOCK_OBJECT_1_DETAILS_RESPONSE); + .reply(200, null, MOCK_OBJECT_1_DETAILS_RESPONSE) + + .head('/qc/test/object/12/1728058750070/1728058895900/baffe0b2-826c-11ef-8f19-c0a80209250c') + .reply(200, null, MOCK_OBJECT_12_DETAILS_RESPONSE); nock(CCDB_URL, xFieldHeader2).persist() .get(CCDB_API_PATH_OBJECT_IDENTIFICATION) @@ -126,7 +131,16 @@ export const initializeNockForCcdb = () => { .reply(200, MOCK_OBJECT_IDENTIFICATION_RESPONSE) .get(`${CCDB_API_PATH_TREE}/object/1`) - .reply(200, MOCK_OBJECT_IDENTIFICATION_RESPONSE); + .reply(200, MOCK_OBJECT_IDENTIFICATION_RESPONSE) + + .get(`${CCDB_API_PATH_TREE}/object/12`) + .reply(200, MOCK_OBJECT_12_IDENTIFICATION_RESPONSE) + + .get(`${CCDB_API_PATH_LATEST}/object/12`) + .reply(200, MOCK_OBJECT_12_IDENTIFICATION_RESPONSE) + + .get ('/latest/qc/test/object/12') + .reply(200, MOCK_OBJECT_12_IDENTIFICATION_RESPONSE); nock(CCDB_URL, xFieldHeader3) .persist() @@ -134,12 +148,18 @@ export const initializeNockForCcdb = () => { .reply(200, MOCK_OBJECT_VERSIONS_RESPONSE) .get('/browse/qc/test/object/1/RunNumber=0') - .reply(200, MOCK_OBJECT_VERSIONS_RESPONSE_RUN_NUMBER_FILTER); + .reply(200, MOCK_OBJECT_VERSIONS_RESPONSE_RUN_NUMBER_FILTER) + + .get('/browse/qc/test/object/12') + .reply(200, MOCK_OBJECT_12_VERSIONS_RESPONSE); nock(CCDB_URL) .persist() .replyContentLength() .get(`${CCDB_API_DOWNLOAD_ROOT_OBJECT.path}/${CCDB_API_DOWNLOAD_ROOT_OBJECT.id}`) + .reply(200, fileContent) + + .get('/download/baffe0b2-826c-11ef-8f19-c0a80209250c') .reply(200, fileContent); //runs mode