From 7b346c8b10e49cd483bdf3b12fca364fe35e1cc8 Mon Sep 17 00:00:00 2001 From: crashdance Date: Mon, 8 Dec 2025 00:18:57 +0100 Subject: [PATCH 01/14] feature: add toggle button for objectInfoPanel and add drawingOptions panel to objectViewPage --- QualityControl/public/common/chevronButton.js | 53 +++++++ .../public/common/constants/drawingOptions.js | 21 +++ .../common/object/objectDrawingOptions.js | 102 +++++++++++++ .../public/common/object/objectInfoCard.js | 91 +++++++++++- .../public/common/visibilityButton.js | 36 ++--- .../pages/objectView/ObjectViewModel.js | 140 +++++++++++++++++- .../public/pages/objectView/ObjectViewPage.js | 65 ++++---- .../object-view-from-layout-show.test.js | 44 +++--- 8 files changed, 467 insertions(+), 85 deletions(-) create mode 100644 QualityControl/public/common/chevronButton.js create mode 100644 QualityControl/public/common/constants/drawingOptions.js create mode 100644 QualityControl/public/common/object/objectDrawingOptions.js 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..e133f0353 --- /dev/null +++ b/QualityControl/public/common/constants/drawingOptions.js @@ -0,0 +1,21 @@ +/** + * @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 const DRAWING_OPTIONS = Object.freeze({ + DRAW_OPTIONS: DRAW_OPTIONS, + DISPLAY_HINTS: DISPLAY_HINTS, +}); diff --git a/QualityControl/public/common/object/objectDrawingOptions.js b/QualityControl/public/common/object/objectDrawingOptions.js new file mode 100644 index 000000000..1c1a4d307 --- /dev/null +++ b/QualityControl/public/common/object/objectDrawingOptions.js @@ -0,0 +1,102 @@ +/** + * @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 { DRAWING_OPTIONS } 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.nonRecognizedOptions - 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, + nonRecognizedOptions, + onToggleIgnoreDefaults, + onToggleOption, +}) => { + const { DRAW_OPTIONS, DISPLAY_HINTS } = DRAWING_OPTIONS; + return h('.absolute-fill.level1.scroll-y', [ + 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}defaults`, + 'Ignore defaults', + 'Set by ROOT (fOption) and QC Metadata', + ignoreDefaults, + onToggleIgnoreDefaults, + ), + ]), + nonRecognizedOptions.length > 0 && + h( + '.flex-row.label.mv2.danger', + 'Non-recognized options: ', + nonRecognizedOptions.join(', '), + ), + h('', [ + 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, label, checked, onChange) => + h('.form-check', [ + h('input.form-check-input', { type: 'checkbox', id, checked, onchange: onChange }), + h('label.m0', { for: id }, label), + ]); + +const checkboxWithTooltip = (id, label, tooltipText, checked, onChange) => + h('.form-check.tooltip.mt2-sm.mh2', [ + h('input.form-check-input', { type: 'checkbox', 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..a258ac849 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,12 +42,141 @@ export default class ObjectViewModel extends BaseViewModel { */ this.selected = RemoteData.notAsked(); - this.drawingOptions = []; - this.displayHints = []; - this.ignoreDefaults = false; + this._drawingOptions = { + options: [], + ignoreDefaults: false, + drawOptions: [], + displayHints: [], + layoutDisplayOptions: [], + nonRecognizedOptions: [], + }; + /** + * Tracks whether the object information panel is currently visible. + */ + this._objectInfoVisible = true; this._storage = new BrowserStorage(StorageKeysEnum.OBJECT_VIEW_INFO_VISIBILITY_SETTING); this._loadObjectInfoVisible(); + + this._objectDrawingOptionsVisible = false; + } + + get drawingOptions() { + return this._drawingOptions.options; + } + + get ignoreDefaults() { + return this._drawingOptions.ignoreDefaults; + } + + get objectDrawingOptionsVisible() { + return this._objectDrawingOptionsVisible; + } + + /** + * Update the drawing options based on the current ignoreDefaults setting. + */ + _setDrawingOptions() { + const options = this.ignoreDefaults + ? [...this._drawingOptions.layoutDisplayOptions] + : [ + ...this._drawingOptions.drawOptions, + ...this._drawingOptions.displayHints, + ...this._drawingOptions.layoutDisplayOptions, + ]; + this._drawingOptions.options = options; + this.notify(); + } + + /** + * Toggle a specific drawing option on or off. + * @param {string} option - the drawing option to toggle + */ + toggleDrawingOption(option) { + const index = this._drawingOptions.options.indexOf(option); + if (index >= 0) { + this._drawingOptions.options.splice(index, 1); + } else { + this._drawingOptions.options.push(option); + } + this.notify(); + } + + /** + * Toggle the ignoreDefaults and update drawing options. + */ + toggleIgnoreDefaults() { + this._drawingOptions.ignoreDefaults = !this._drawingOptions.ignoreDefaults; + this._setDrawingOptions(); + 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(); + } + + /** + * Initializes the non-recognized drawing options by filtering out known options + * from the current drawing options and display hints. + */ + _initializeNonRecognizedOptions() { + const recognizedDrawOptions = DRAWING_OPTIONS.DRAW_OPTIONS; + const recognizedDisplayHints = DRAWING_OPTIONS.DISPLAY_HINTS; + this._drawingOptions.nonRecognizedOptions = [ + ...this._drawingOptions.drawOptions.filter((option) => !recognizedDrawOptions.includes(option)), + ...this._drawingOptions.displayHints.filter((option) => !recognizedDisplayHints.includes(option)), + ]; + this.notify(); + } + + /** + * Initializes the drawing options based on the context from which the object was opened. + * If opened from a layout view, it uses the layout/tab configuration; otherwise, + * it uses the object's own configuration. + */ + async _initializeDrawingOptions() { + const { layoutId, objectId } = this.model.router.params; + const { qcObject } = this.selected.payload; + this._drawingOptions = { + ignoreDefaults: Boolean(qcObject.ignoreDefaults), + drawOptions: [...qcObject.drawOptions || []], + displayHints: [...qcObject.displayHints || []], + layoutDisplayOptions: [...qcObject.layoutDisplayOptions || []], + }; + if (layoutId && objectId) { + // Object opened from layout view -> use the layout/tab configuration + const layoutResult = await this.model.services.layout.getLayoutById(layoutId); + if (layoutResult.isSuccess()) { + const layout = layoutResult.payload; + const object = layout.tabs.flatMap((tab) => tab.objects).find((obj) => obj.id === objectId); + if (object) { + const { + ignoreDefaults: objectIgnoreDefaults = false, + drawOptions: objectDrawOptions = [], + displayHints: objectDisplayHints = [], + options: objectLayoutDisplayOptions = [], + } = object; + this._drawingOptions = { + ignoreDefaults: Boolean(objectIgnoreDefaults), + drawOptions: [...objectDrawOptions || []], + displayHints: [...objectDisplayHints || []], + layoutDisplayOptions: [...objectLayoutDisplayOptions || []], + }; + this._setDrawingOptions(); + this.notify(); + } + } + } else { + // Object opened directly -> use the object configuration + this._initializeNonRecognizedOptions(); + this._setDrawingOptions(); + this.notify(); + } } /** @@ -116,6 +246,10 @@ export default class ObjectViewModel extends BaseViewModel { this.model.router.go(`${currentParams}`, false, true); } + if (this.selected.isSuccess()) { + this._initializeDrawingOptions(); + } + this.notify(); } diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index b296c716c..ea8461d60 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,13 @@ 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, + 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 +64,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 or drawing options panel + 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, + nonRecognizedOptions: [], + 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/public/pages/object-view-from-layout-show.test.js b/QualityControl/test/public/pages/object-view-from-layout-show.test.js index 07b65f280..80e06be4f 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); From d4f4cb5d4bb2ffe74c7760164bc3512bbcae9f84 Mon Sep 17 00:00:00 2001 From: crashdance Date: Fri, 12 Dec 2025 13:26:26 +0100 Subject: [PATCH 02/14] fix: drawing options handling and setting non-recognized options --- .../public/common/constants/drawingOptions.js | 7 +- .../common/object/objectDrawingOptions.js | 32 +-- .../pages/objectView/ObjectViewModel.js | 207 +++++++----------- .../public/pages/objectView/ObjectViewPage.js | 3 +- 4 files changed, 89 insertions(+), 160 deletions(-) diff --git a/QualityControl/public/common/constants/drawingOptions.js b/QualityControl/public/common/constants/drawingOptions.js index e133f0353..623be5ee2 100644 --- a/QualityControl/public/common/constants/drawingOptions.js +++ b/QualityControl/public/common/constants/drawingOptions.js @@ -15,7 +15,6 @@ const DRAW_OPTIONS = ['lego', 'colz', 'lcolz', 'text']; const DISPLAY_HINTS = ['logx', 'logy', 'logz', 'gridx', 'gridy', 'gridz', 'stat']; -export const DRAWING_OPTIONS = Object.freeze({ - DRAW_OPTIONS: DRAW_OPTIONS, - DISPLAY_HINTS: DISPLAY_HINTS, -}); +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 index 1c1a4d307..5185e8f59 100644 --- a/QualityControl/public/common/object/objectDrawingOptions.js +++ b/QualityControl/public/common/object/objectDrawingOptions.js @@ -13,7 +13,8 @@ */ import { h } from '/js/src/index.js'; -import { DRAWING_OPTIONS } from '../constants/drawingOptions.js'; +import { DRAW_OPTIONS } from '../constants/drawingOptions.js'; +import { DISPLAY_HINTS } from '../constants/drawingOptions.js'; /** * Display options overlay for a QC object @@ -21,7 +22,7 @@ import { DRAWING_OPTIONS } from '../constants/drawingOptions.js'; * @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.nonRecognizedOptions - Array of non-recognized drawing options + * @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 @@ -30,12 +31,11 @@ export const objectDrawingOptions = ({ id, ignoreDefaults, options, - nonRecognizedOptions, + nonRecognizedDrawingOptions, onToggleIgnoreDefaults, onToggleOption, -}) => { - const { DRAW_OPTIONS, DISPLAY_HINTS } = DRAWING_OPTIONS; - return h('.absolute-fill.level1.scroll-y', [ +}) => + h('.absolute-fill.level1.scroll-y', [ 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:'), @@ -47,33 +47,21 @@ export const objectDrawingOptions = ({ onToggleIgnoreDefaults, ), ]), - nonRecognizedOptions.length > 0 && + nonRecognizedDrawingOptions && h( '.flex-row.label.mv2.danger', - 'Non-recognized options: ', - nonRecognizedOptions.join(', '), + `Non-recognized options: ${nonRecognizedDrawingOptions.join(', ')}`, ), h('', [ sectionTitle('Draw Options:', ' ROOT draw options'), checkboxGrid(DRAW_OPTIONS.map((option) => - checkBox( - id + option, - option, - options.includes(option), - () => onToggleOption(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), - ))), + checkBox(id + option, option, options.includes(option), () => onToggleOption(option)))), ]), ]), ]); -}; const checkboxGrid = (children) => h('.flex-column.g2', { diff --git a/QualityControl/public/pages/objectView/ObjectViewModel.js b/QualityControl/public/pages/objectView/ObjectViewModel.js index a258ac849..f2f4d2a5b 100644 --- a/QualityControl/public/pages/objectView/ObjectViewModel.js +++ b/QualityControl/public/pages/objectView/ObjectViewModel.js @@ -42,14 +42,16 @@ export default class ObjectViewModel extends BaseViewModel { */ this.selected = RemoteData.notAsked(); - this._drawingOptions = { - options: [], - ignoreDefaults: false, - drawOptions: [], - displayHints: [], - layoutDisplayOptions: [], - nonRecognizedOptions: [], - }; + /** + * 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. @@ -57,126 +59,6 @@ export default class ObjectViewModel extends BaseViewModel { this._objectInfoVisible = true; this._storage = new BrowserStorage(StorageKeysEnum.OBJECT_VIEW_INFO_VISIBILITY_SETTING); this._loadObjectInfoVisible(); - - this._objectDrawingOptionsVisible = false; - } - - get drawingOptions() { - return this._drawingOptions.options; - } - - get ignoreDefaults() { - return this._drawingOptions.ignoreDefaults; - } - - get objectDrawingOptionsVisible() { - return this._objectDrawingOptionsVisible; - } - - /** - * Update the drawing options based on the current ignoreDefaults setting. - */ - _setDrawingOptions() { - const options = this.ignoreDefaults - ? [...this._drawingOptions.layoutDisplayOptions] - : [ - ...this._drawingOptions.drawOptions, - ...this._drawingOptions.displayHints, - ...this._drawingOptions.layoutDisplayOptions, - ]; - this._drawingOptions.options = options; - this.notify(); - } - - /** - * Toggle a specific drawing option on or off. - * @param {string} option - the drawing option to toggle - */ - toggleDrawingOption(option) { - const index = this._drawingOptions.options.indexOf(option); - if (index >= 0) { - this._drawingOptions.options.splice(index, 1); - } else { - this._drawingOptions.options.push(option); - } - this.notify(); - } - - /** - * Toggle the ignoreDefaults and update drawing options. - */ - toggleIgnoreDefaults() { - this._drawingOptions.ignoreDefaults = !this._drawingOptions.ignoreDefaults; - this._setDrawingOptions(); - 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(); - } - - /** - * Initializes the non-recognized drawing options by filtering out known options - * from the current drawing options and display hints. - */ - _initializeNonRecognizedOptions() { - const recognizedDrawOptions = DRAWING_OPTIONS.DRAW_OPTIONS; - const recognizedDisplayHints = DRAWING_OPTIONS.DISPLAY_HINTS; - this._drawingOptions.nonRecognizedOptions = [ - ...this._drawingOptions.drawOptions.filter((option) => !recognizedDrawOptions.includes(option)), - ...this._drawingOptions.displayHints.filter((option) => !recognizedDisplayHints.includes(option)), - ]; - this.notify(); - } - - /** - * Initializes the drawing options based on the context from which the object was opened. - * If opened from a layout view, it uses the layout/tab configuration; otherwise, - * it uses the object's own configuration. - */ - async _initializeDrawingOptions() { - const { layoutId, objectId } = this.model.router.params; - const { qcObject } = this.selected.payload; - this._drawingOptions = { - ignoreDefaults: Boolean(qcObject.ignoreDefaults), - drawOptions: [...qcObject.drawOptions || []], - displayHints: [...qcObject.displayHints || []], - layoutDisplayOptions: [...qcObject.layoutDisplayOptions || []], - }; - if (layoutId && objectId) { - // Object opened from layout view -> use the layout/tab configuration - const layoutResult = await this.model.services.layout.getLayoutById(layoutId); - if (layoutResult.isSuccess()) { - const layout = layoutResult.payload; - const object = layout.tabs.flatMap((tab) => tab.objects).find((obj) => obj.id === objectId); - if (object) { - const { - ignoreDefaults: objectIgnoreDefaults = false, - drawOptions: objectDrawOptions = [], - displayHints: objectDisplayHints = [], - options: objectLayoutDisplayOptions = [], - } = object; - this._drawingOptions = { - ignoreDefaults: Boolean(objectIgnoreDefaults), - drawOptions: [...objectDrawOptions || []], - displayHints: [...objectDisplayHints || []], - layoutDisplayOptions: [...objectLayoutDisplayOptions || []], - }; - this._setDrawingOptions(); - this.notify(); - } - } - } else { - // Object opened directly -> use the object configuration - this._initializeNonRecognizedOptions(); - this._setDrawingOptions(); - this.notify(); - } } /** @@ -205,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; @@ -224,10 +109,15 @@ export default class ObjectViewModel extends BaseViewModel { // Use refreshed data if available, otherwise fetch based on available parameters if (data) { this.selected = data; + this._initialDrawingOptions(); } else if (params.objectName) { - this.selected = await this.model.services.object.getObjectByName(params.objectName, id, validFrom, this); + this.selected = + await this.model.services.object.getObjectByName(params.objectName, id, validFrom, this); + this._initialDrawingOptions(); } else if (params.objectId) { - this.selected = await this.model.services.object.getObjectById(params.objectId, id, validFrom, this); + this.selected = + await this.model.services.object.getObjectById(params.objectId, id, validFrom, this); + this._initialDrawingOptions(); } setBrowserTabTitle(this.selected.payload.name); @@ -246,10 +136,52 @@ export default class ObjectViewModel extends BaseViewModel { this.model.router.go(`${currentParams}`, false, true); } - if (this.selected.isSuccess()) { - this._initializeDrawingOptions(); + this.notify(); + } + + _initialDrawingOptions() { + const { ignoreDefaults, drawOptions, displayHints, layoutDisplayOptions } = this.selected.payload; + this.ignoreDefaults = Boolean(ignoreDefaults); + this.drawOptions = drawOptions || []; + this.displayHints = displayHints || []; + this.layoutDisplayOptions = layoutDisplayOptions || []; + if (this.ignoreDefaults) { + this.drawingOptions = [...this.layoutDisplayOptions]; + } else { + this.drawingOptions = [...this.drawOptions || [], ...this.displayHints || [], ...this.layoutDisplayOptions || []]; } + const allDrawingOptions = new Set((this.drawOptions || []) + .concat(this.displayHints || [], this.layoutDisplayOptions || [])); + this.nonRecognizedDrawingOptions = [...allDrawingOptions].filter((option) => !DRAWING_OPTIONS.has(option)); + this.notify(); + } + /** + * Toggle the ignoreDefaults for drawing options. + * When ignored, default drawing options on object will not be applied. + */ + toggleIgnoreDefaults() { + this.ignoreDefaults = !this.ignoreDefaults; + if (this.ignoreDefaults) { + this.drawingOptions = [...this.layoutDisplayOptions]; + } else { + this.drawingOptions = [...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(); } @@ -299,6 +231,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 ea8461d60..8f6c8b2cd 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -46,6 +46,7 @@ const objectPlotAndInfo = (objectViewModel) => const { ignoreDefaults, drawingOptions, + nonRecognizedDrawingOptions, objectInfoVisible, objectDrawingOptionsVisible, } = objectViewModel; @@ -86,7 +87,7 @@ const objectPlotAndInfo = (objectViewModel) => id, ignoreDefaults: ignoreDefaults, options: drawingOptions, - nonRecognizedOptions: [], + nonRecognizedDrawingOptions: nonRecognizedDrawingOptions, onToggleIgnoreDefaults: () => objectViewModel.toggleIgnoreDefaults(), onToggleOption: (option) => objectViewModel.toggleDrawingOption(option), }), From 9d00c594723cdb80e899cd0fcccdf3e8358728c3 Mon Sep 17 00:00:00 2001 From: crashdance Date: Fri, 12 Dec 2025 14:49:14 +0100 Subject: [PATCH 03/14] fix: ensure non-recognized drawing options are displayed only when present --- QualityControl/public/common/object/objectDrawingOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/common/object/objectDrawingOptions.js b/QualityControl/public/common/object/objectDrawingOptions.js index 5185e8f59..6d77541d2 100644 --- a/QualityControl/public/common/object/objectDrawingOptions.js +++ b/QualityControl/public/common/object/objectDrawingOptions.js @@ -47,7 +47,7 @@ export const objectDrawingOptions = ({ onToggleIgnoreDefaults, ), ]), - nonRecognizedDrawingOptions && + nonRecognizedDrawingOptions && nonRecognizedDrawingOptions.length > 0 && h( '.flex-row.label.mv2.danger', `Non-recognized options: ${nonRecognizedDrawingOptions.join(', ')}`, From 97e9b52e16bcca2636b8ba2dfb1d664b43928471 Mon Sep 17 00:00:00 2001 From: crashdance Date: Mon, 15 Dec 2025 15:34:51 +0100 Subject: [PATCH 04/14] fix: update initial drawing options handling and improve logic --- .../common/object/objectDrawingOptions.js | 55 ++++++++++--------- .../pages/objectView/ObjectViewModel.js | 49 +++++++++-------- .../public/pages/objectView/ObjectViewPage.js | 2 +- 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/QualityControl/public/common/object/objectDrawingOptions.js b/QualityControl/public/common/object/objectDrawingOptions.js index 6d77541d2..0e64ddda3 100644 --- a/QualityControl/public/common/object/objectDrawingOptions.js +++ b/QualityControl/public/common/object/objectDrawingOptions.js @@ -35,31 +35,26 @@ export const objectDrawingOptions = ({ onToggleIgnoreDefaults, onToggleOption, }) => - h('.absolute-fill.level1.scroll-y', [ + 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}defaults`, - 'Ignore defaults', - 'Set by ROOT (fOption) and QC Metadata', - ignoreDefaults, - onToggleIgnoreDefaults, - ), + 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(', ')}`, - ), - h('', [ - 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)))), - ]), + 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)))), ]), ]); @@ -76,15 +71,25 @@ const checkboxGrid = (children) => const sectionTitle = (label, tooltipText) => h('.flex-row.mv2', h('.tooltip', [h('label.m0', label), h('.tooltiptext', tooltipText)])); -const checkBox = (id, label, checked, onChange) => +const checkBox = (id, option, checked, onChange) => h('.form-check', [ - h('input.form-check-input', { type: 'checkbox', id, checked, onchange: onChange }), - h('label.m0', { for: id }, label), + 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) => +const checkboxWithTooltip = ({ id, label, tooltipText, checked, onChange }) => h('.form-check.tooltip.mt2-sm.mh2', [ - h('input.form-check-input', { type: 'checkbox', id, checked, onchange: onChange }), + 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/pages/objectView/ObjectViewModel.js b/QualityControl/public/pages/objectView/ObjectViewModel.js index f2f4d2a5b..11c209b13 100644 --- a/QualityControl/public/pages/objectView/ObjectViewModel.js +++ b/QualityControl/public/pages/objectView/ObjectViewModel.js @@ -109,17 +109,13 @@ export default class ObjectViewModel extends BaseViewModel { // Use refreshed data if available, otherwise fetch based on available parameters if (data) { this.selected = data; - this._initialDrawingOptions(); } else if (params.objectName) { - this.selected = - await this.model.services.object.getObjectByName(params.objectName, id, validFrom, this); - this._initialDrawingOptions(); + this.selected = await this.model.services.object.getObjectByName(params.objectName, id, validFrom, this); } else if (params.objectId) { - this.selected = - await this.model.services.object.getObjectById(params.objectId, id, validFrom, this); - this._initialDrawingOptions(); + 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]) => { @@ -139,20 +135,27 @@ export default class ObjectViewModel extends BaseViewModel { this.notify(); } + /** + * Set the initial drawing options based on the selected object + */ _initialDrawingOptions() { - const { ignoreDefaults, drawOptions, displayHints, layoutDisplayOptions } = this.selected.payload; + const { + ignoreDefaults = false, + drawOptions = [], + displayHints = [], + layoutDisplayOptions = [], + } = this.selected.payload; this.ignoreDefaults = Boolean(ignoreDefaults); - this.drawOptions = drawOptions || []; - this.displayHints = displayHints || []; - this.layoutDisplayOptions = layoutDisplayOptions || []; - if (this.ignoreDefaults) { - this.drawingOptions = [...this.layoutDisplayOptions]; - } else { - this.drawingOptions = [...this.drawOptions || [], ...this.displayHints || [], ...this.layoutDisplayOptions || []]; - } - const allDrawingOptions = new Set((this.drawOptions || []) - .concat(this.displayHints || [], this.layoutDisplayOptions || [])); - this.nonRecognizedDrawingOptions = [...allDrawingOptions].filter((option) => !DRAWING_OPTIONS.has(option)); + 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(); } @@ -162,11 +165,9 @@ export default class ObjectViewModel extends BaseViewModel { */ toggleIgnoreDefaults() { this.ignoreDefaults = !this.ignoreDefaults; - if (this.ignoreDefaults) { - this.drawingOptions = [...this.layoutDisplayOptions]; - } else { - this.drawingOptions = [...this.drawOptions || [], ...this.displayHints || [], ...this.layoutDisplayOptions || []]; - } + this.drawingOptions = this.ignoreDefaults + ? [...this.layoutDisplayOptions] + : [...this.drawOptions, ...this.displayHints, ...this.layoutDisplayOptions]; this.notify(); } diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index 8f6c8b2cd..92f7b1a36 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -73,7 +73,7 @@ const objectPlotAndInfo = (objectViewModel) => ]), h('.flex-row.g2.m2.flex-grow', [ h('.flex-grow', { - // force redraw on toggle info panel or drawing options panel + // force redraw on toggle info panel and update drawing options key: `${objectInfoVisible}-${drawingOptions}`, }, drawObject(qcObject, {}, drawingOptions, (error) => { objectViewModel.drawingFailureOccurred(error.message); From a0f6923a2ad21ba0b83f664d8cabc6a183737f8c Mon Sep 17 00:00:00 2001 From: crashdance Date: Wed, 17 Dec 2025 14:06:38 +0100 Subject: [PATCH 05/14] tests: add drawing options panel visibility tests and update plot update on toggle drawing option --- .../object-view-from-object-tree.test.js | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) 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..e36ebb038 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,91 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t }, ); + 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 = '016fa8ac-f3b6-11ec-b9a9-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 ignore defaults checkbox unchecked by default', + { timeout }, + async () => { + const objectId = '016fa8ac-f3b6-11ec-b9a9-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 = '016fa8ac-f3b6-11ec-b9a9-c0a80209250c'; + const plotSelector = '#ObjectPlot > div:nth-child(2) > div > div'; + const gridXCheckboxSelector = `#objectDrawingOptions input[id="${objectId}gridx"]`; + const initialPlot = await page.waitForSelector(plotSelector, { timeout: 1000 }); + const checkboxExists = await page.evaluate((selector) => + Boolean(document.querySelector(selector)), gridXCheckboxSelector); + strictEqual(checkboxExists, true, '"gridx" drawing option checkbox not found'); + const initalGridXEnabled = await page.evaluate((selector) => + document.querySelector(selector).checked, gridXCheckboxSelector); + + await page.click(gridXCheckboxSelector); + await delay(100); + const gridXEnabled = await page.evaluate((selector) => + document.querySelector(selector).checked, gridXCheckboxSelector); + const afterTogglePlot = await page.waitForSelector(plotSelector, { timeout: 1000 }); + const redrawn = await page.evaluate((a, b) => a.innerHTML !== b.innerHTML, initialPlot, afterTogglePlot); + + strictEqual(checkboxExists, true, '"gridx" drawing option checkbox not found'); + strictEqual(initalGridXEnabled, false, '"gridx" drawing option should be initially disabled'); + strictEqual(gridXEnabled, true, '"gridx" drawing option should be enabled after toggle'); + strictEqual(redrawn, true, 'JSRoot drawing was not redrawn on object info panel visibility change'); + }, + ); + + 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( From 488c811d432cb6ecbe9c4a6eae4b685a4f0fb364 Mon Sep 17 00:00:00 2001 From: crashdance Date: Wed, 17 Dec 2025 23:30:46 +0100 Subject: [PATCH 06/14] test: update filter tests and drawing options tests for object view from object tree --- .../test/public/features/filterTest.test.js | 4 +- .../object-view-from-object-tree.test.js | 85 ++++++++++++++----- .../test/setup/seeders/ccdbObjects.js | 49 +++++++++++ .../seeders/object-view/mock-object-view.js | 61 +++++++++++++ QualityControl/test/setup/testSetupForCcdb.js | 25 +++++- 5 files changed, 198 insertions(+), 26 deletions(-) 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/object-view-from-object-tree.test.js b/QualityControl/test/public/pages/object-view-from-object-tree.test.js index e36ebb038..14cf47877 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 @@ -88,6 +88,23 @@ 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'; + 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 }; + }); + strictEqual(result.fingerprint.includes('hist'), true); + strictEqual(result.fingerprint.includes('gridy'), true); + strictEqual(result.fingerprint.includes('text'), true); + }, + ); + await testParent.test( 'should initially hide drawing options panel', { timeout }, @@ -112,7 +129,7 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t 'should display ignore defaults button', { timeout }, async () => { - const objectId = '016fa8ac-f3b6-11ec-b9a9-c0a80209250c'; + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; const checkboxId = `${objectId}ignoreDefaults`; const ignoreDefaultsCheckboxSelector = `#objectDrawingOptions input[id="${checkboxId}"]`; const element = await page.evaluate((selector) => @@ -121,11 +138,32 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t }, ); + 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 = '016fa8ac-f3b6-11ec-b9a9-c0a80209250c'; + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; const checkboxId = `${objectId}ignoreDefaults`; const ignoreDefaultsCheckboxSelector = `#objectDrawingOptions input[id="${checkboxId}"]`; const checked = await page.evaluate((selector) => @@ -138,27 +176,34 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t 'should update plot when a drawing option is toggled', { timeout }, async () => { - const objectId = '016fa8ac-f3b6-11ec-b9a9-c0a80209250c'; - const plotSelector = '#ObjectPlot > div:nth-child(2) > div > div'; + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; const gridXCheckboxSelector = `#objectDrawingOptions input[id="${objectId}gridx"]`; - const initialPlot = await page.waitForSelector(plotSelector, { timeout: 1000 }); - const checkboxExists = await page.evaluate((selector) => - Boolean(document.querySelector(selector)), gridXCheckboxSelector); - strictEqual(checkboxExists, true, '"gridx" drawing option checkbox not found'); - const initalGridXEnabled = await page.evaluate((selector) => - document.querySelector(selector).checked, gridXCheckboxSelector); + + 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(100); - const gridXEnabled = await page.evaluate((selector) => - document.querySelector(selector).checked, gridXCheckboxSelector); - const afterTogglePlot = await page.waitForSelector(plotSelector, { timeout: 1000 }); - const redrawn = await page.evaluate((a, b) => a.innerHTML !== b.innerHTML, initialPlot, afterTogglePlot); - - strictEqual(checkboxExists, true, '"gridx" drawing option checkbox not found'); - strictEqual(initalGridXEnabled, false, '"gridx" drawing option should be initially disabled'); - strictEqual(gridXEnabled, true, '"gridx" drawing option should be enabled after toggle'); - strictEqual(redrawn, true, 'JSRoot drawing was not redrawn on object info panel visibility change'); + 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', + ); }, ); diff --git a/QualityControl/test/setup/seeders/ccdbObjects.js b/QualityControl/test/setup/seeders/ccdbObjects.js index 04066287a..8c8429504 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..7c15c0720 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/testSetupForCcdb.js b/QualityControl/test/setup/testSetupForCcdb.js index f3bc28eff..f8afee53e 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,13 @@ 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_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 +145,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 From b1c2ddc282f06ea08bdf8c2d9662841456bcdad3 Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 13:31:36 +0100 Subject: [PATCH 07/14] tests: object view from layout view for drawing options --- .../test/public/pages/layout-list.test.js | 4 +- .../object-view-from-layout-show.test.js | 155 ++++++++++++++++++ .../object-view-from-object-tree.test.js | 6 +- .../setup/seeders/qcg-mock-data-template.json | 30 ++++ QualityControl/test/setup/testSetupForCcdb.js | 3 + 5 files changed, 193 insertions(+), 5 deletions(-) diff --git a/QualityControl/test/public/pages/layout-list.test.js b/QualityControl/test/public/pages/layout-list.test.js index 8308a6013..25a0e1fd7 100644 --- a/QualityControl/test/public/pages/layout-list.test.js +++ b/QualityControl/test/public/pages/layout-list.test.js @@ -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, 6); 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, 4); await page.locator(filterPath).fill('pdpBeamType'); await delay(100); 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 80e06be4f..3f5445149 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 @@ -409,6 +409,161 @@ 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' }, + ); + 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 -> load page=objectView and display a plot when objectId and layoutId are passed', + // { timeout }, + // async () => { + // const result = await page.evaluate(() => { + // const title = document.querySelector('div div b').textContent; + // const rootPlotClassList = document + // .querySelector('#ObjectPlot > div:nth-child(2) > div:nth-child(1) > div').classList; + // const selectedObjectPath = window.model.objectViewModel.selected.payload.path; + // return { + // title, rootPlotClassList, selectedObjectPath, + // }; + // }); + // strictEqual(result.title, 'qc/test/object/12 (from layout: drawing-test)'); + // deepStrictEqual(result.rootPlotClassList, { 0: 'relative', 1: 'jsroot-container' }); + // strictEqual(result.selectedObjectPath, 'qc/test/object/12'); + // }, + // ); + + 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 objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const expectedDrawingOptions = ['logx', 'text']; + const activeOptions = await page.evaluate((id) => { + const checkboxes = Array.from( + document.querySelectorAll(`#objectDrawingOptions input[type="checkbox"][id^="${id}"]`), + ); + return checkboxes + .filter((checkbox) => checkbox.checked) + .map((checkbox) => checkbox.id.replace(id, '')); + }, objectId); + deepStrictEqual( + activeOptions.sort(), + expectedDrawingOptions.sort(), + 'Active drawing options do not match expected layout settings', + ); + }, + ); + + await testParent.test( + 'should set active checkboxes from combined drawing options when ignore defaults is set to false', + { timeout }, + async () => { + const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; + const expectedDrawingOptions = ['logx', 'text', 'hist', 'gridy']; + const ignoreDefaultsCheckboxSelector = `#objectDrawingOptions input[id="${objectId}ignoreDefaults"]`; + + await page.click(ignoreDefaultsCheckboxSelector); + await delay(100); + + const activeOptions = await page.evaluate((id) => { + const checkboxes = Array.from( + document.querySelectorAll(`#objectDrawingOptions input[type="checkbox"][id^="${id}"]`), + ); + return checkboxes + .filter((checkbox) => checkbox.checked) + .map((checkbox) => checkbox.id.replace(id, '')); + }, objectId); + deepStrictEqual( + activeOptions.sort(), + expectedDrawingOptions.sort(), + 'Active drawing options do not match expected layout settings', + ); + }, + ); + + await testParent.test( + 'should have updated fingerprint 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 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 14cf47877..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 @@ -92,6 +92,7 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t '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; @@ -99,9 +100,8 @@ export const objectViewFromObjectTreeTests = async (url, page, timeout = 5000, t const fingerprint = plotElement.dataset.fingerprintData; return { fingerprint, drawingOptions }; }); - strictEqual(result.fingerprint.includes('hist'), true); - strictEqual(result.fingerprint.includes('gridy'), true); - strictEqual(result.fingerprint.includes('text'), true); + const allOptionsPresent = expectedDrawingOptions.every((option) => result.fingerprint.includes(option)); + strictEqual(allOptionsPresent, true, 'Not all expected drawing options are present in the plot fingerprint'); }, ); 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 f8afee53e..f1b7ad726 100644 --- a/QualityControl/test/setup/testSetupForCcdb.js +++ b/QualityControl/test/setup/testSetupForCcdb.js @@ -133,6 +133,9 @@ export const initializeNockForCcdb = () => { .get(`${CCDB_API_PATH_TREE}/object/1`) .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) From 573a38667ce67b8355a2a82c216de33d9bb7707d Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 13:32:28 +0100 Subject: [PATCH 08/14] tests: fix linting --- .../test/public/pages/object-view-from-layout-show.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3f5445149..1607e546c 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 @@ -408,7 +408,7 @@ 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 }, From 2b07cbbc90c1fea99bd4fcb346c915aef9ae6434 Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 13:35:27 +0100 Subject: [PATCH 09/14] tests: remove unnecessary comma --- .../test/public/pages/object-view-from-layout-show.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1607e546c..3f5445149 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 @@ -408,7 +408,7 @@ 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 }, From 70603e9c430354326937c0bab870db0134383b7a Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 13:43:25 +0100 Subject: [PATCH 10/14] test: fix expand object details for drawing options panel button access --- .../object-view-from-layout-show.test.js | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) 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 3f5445149..b635209d5 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 @@ -421,7 +421,10 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t `${url}?page=objectView&objectId=${objectId}&layoutId=${layoutId}`, { waitUntil: 'networkidle0' }, ); + // Click the toggle button to show the drawing options panel + await page.click('.chevron-button'); await delay(100); + const result = await page.evaluate(() => { const { ignoreDefaults } = model.objectViewModel; const { drawingOptions } = model.objectViewModel; @@ -437,25 +440,6 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t }, ); - // await testParent.test( - // 'should -> load page=objectView and display a plot when objectId and layoutId are passed', - // { timeout }, - // async () => { - // const result = await page.evaluate(() => { - // const title = document.querySelector('div div b').textContent; - // const rootPlotClassList = document - // .querySelector('#ObjectPlot > div:nth-child(2) > div:nth-child(1) > div').classList; - // const selectedObjectPath = window.model.objectViewModel.selected.payload.path; - // return { - // title, rootPlotClassList, selectedObjectPath, - // }; - // }); - // strictEqual(result.title, 'qc/test/object/12 (from layout: drawing-test)'); - // deepStrictEqual(result.rootPlotClassList, { 0: 'relative', 1: 'jsroot-container' }); - // strictEqual(result.selectedObjectPath, 'qc/test/object/12'); - // }, - // ); - await testParent.test( 'should initially hide drawing options panel', { timeout }, From 0f1bd54146611fba4c23efd9a257fc8b610750a0 Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 13:44:40 +0100 Subject: [PATCH 11/14] test: clarify comment for drawing options panel toggle button --- .../test/public/pages/object-view-from-layout-show.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b635209d5..b630d7745 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 @@ -421,7 +421,7 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t `${url}?page=objectView&objectId=${objectId}&layoutId=${layoutId}`, { waitUntil: 'networkidle0' }, ); - // Click the toggle button to show the drawing options panel + // Click the toggle button to show object details panel containing drawing options visibility button await page.click('.chevron-button'); await delay(100); From 67d1b411af6fd68be1233150f3be01dd8226d76f Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 15:55:46 +0100 Subject: [PATCH 12/14] test: fix and add layout tests for drawing options on object view page --- .../object-view-from-layout-show.test.js | 44 +++++++------------ .../test/setup/seeders/ccdbObjects.js | 2 +- .../seeders/object-view/mock-object-view.js | 2 +- 3 files changed, 19 insertions(+), 29 deletions(-) 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 b630d7745..56dd7aaf4 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 @@ -478,18 +478,15 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t 'should set active checkboxes from layout display options when ignore defaults is true', { timeout }, async () => { - const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; - const expectedDrawingOptions = ['logx', 'text']; - const activeOptions = await page.evaluate((id) => { - const checkboxes = Array.from( - document.querySelectorAll(`#objectDrawingOptions input[type="checkbox"][id^="${id}"]`), - ); - return checkboxes + 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) => checkbox.id.replace(id, '')); - }, objectId); + .map((checkbox) => document.querySelector(`#objectDrawingOptions label[for="${checkbox.id}"]`)?.innerText); + }); + const expectedDrawingOptions = ['logx', 'text']; deepStrictEqual( - activeOptions.sort(), + activeCheckboxLabels.sort(), expectedDrawingOptions.sort(), 'Active drawing options do not match expected layout settings', ); @@ -497,26 +494,19 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t ); await testParent.test( - 'should set active checkboxes from combined drawing options when ignore defaults is set to false', + 'should set active checkboxes from layout display options when ignore defaults is set to false', { timeout }, async () => { - const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; - const expectedDrawingOptions = ['logx', 'text', 'hist', 'gridy']; - const ignoreDefaultsCheckboxSelector = `#objectDrawingOptions input[id="${objectId}ignoreDefaults"]`; - - await page.click(ignoreDefaultsCheckboxSelector); - await delay(100); - - const activeOptions = await page.evaluate((id) => { - const checkboxes = Array.from( - document.querySelectorAll(`#objectDrawingOptions input[type="checkbox"][id^="${id}"]`), - ); - return checkboxes + 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) => checkbox.id.replace(id, '')); - }, objectId); + .map((checkbox) => document.querySelector(`#objectDrawingOptions label[for="${checkbox.id}"]`)?.innerText); + }); + const expectedDrawingOptions = ['logx', 'text']; deepStrictEqual( - activeOptions.sort(), + activeCheckboxLabels.sort(), expectedDrawingOptions.sort(), 'Active drawing options do not match expected layout settings', ); @@ -524,7 +514,7 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t ); await testParent.test( - 'should have updated fingerprint after changing drawing options', + 'should updated fingerprint on plot after changing drawing options', { timeout }, async () => { const objectId = 'baffe0b2-826c-11ef-8f19-c0a80209250c'; diff --git a/QualityControl/test/setup/seeders/ccdbObjects.js b/QualityControl/test/setup/seeders/ccdbObjects.js index 8c8429504..db692bdd9 100644 --- a/QualityControl/test/setup/seeders/ccdbObjects.js +++ b/QualityControl/test/setup/seeders/ccdbObjects.js @@ -143,7 +143,7 @@ export const OBJECT_3_BY_PATH_RESULT = { createdAt: 1728058897718, lastModified: 1728058897000, drawOptions: [], - displayHints: ['hist'], + displayHints: 'hist', etag: 'baffe0b2-826c-11ef-8f19-c0a80209250c', runNumber: '551890', runType: 'PHYSICS', 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 7c15c0720..3ec6f965c 100644 --- a/QualityControl/test/setup/seeders/object-view/mock-object-view.js +++ b/QualityControl/test/setup/seeders/object-view/mock-object-view.js @@ -83,7 +83,7 @@ export const MOCK_OBJECT_12_DETAILS_RESPONSE = { objecttype: 'o2::quality_control_modules::common::TH1Ratio', qc_detector_name: 'MFT', drawoptions: 'text', - displayhints: 'hist, gridy', + displayhints: ['hist', 'gridy'], runnumber: '551890', adjustableeov: '1', qc_task_name: 'MFTClusterTask', From d63620bd870a445a3f89ab4c0d874076062dae4f Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 15:56:45 +0100 Subject: [PATCH 13/14] test: add LAYOUT_MOCK_7 and update layout tests with the new mock item --- .../test/api/layouts/api-get-layout.test.js | 8 +++-- .../test/demoData/layout/layout.mock.js | 31 +++++++++++++++++++ .../test/public/pages/layout-list.test.js | 12 +++---- 3 files changed, 43 insertions(+), 8 deletions(-) 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/pages/layout-list.test.js b/QualityControl/test/public/pages/layout-list.test.js index 25a0e1fd7..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, 6); + 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, 4); + 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); From 07f7de98d9c087e0acbcf1963ecdaa3c8bbeb22c Mon Sep 17 00:00:00 2001 From: crashdance Date: Thu, 18 Dec 2025 16:15:11 +0100 Subject: [PATCH 14/14] test: add test for displaying unrecognized drawing options in the drawing options panel --- .../object-view-from-layout-show.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 56dd7aaf4..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 @@ -538,6 +538,25 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t }, ); + 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 },