diff --git a/QualityControl/public/common/downloadRootImageButton.js b/QualityControl/public/common/downloadRootImageButton.js deleted file mode 100644 index 76f69d313..000000000 --- a/QualityControl/public/common/downloadRootImageButton.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @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, imagE } from '/js/src/index.js'; -import { downloadRoot, getFileExtensionFromName } from './utils.js'; -import { isObjectOfTypeChecker } from '../../library/qcObject/utils.js'; - -/** - * Download root image button. - * @param {string} filename - The name of the downloaded file including its extension. - * @param {RootObject} root - The JSROOT RootObject to render. - * @param {string[]} [drawingOptions=[]] - Optional array of JSROOT drawing options. - * @returns {vnode} - Download root image button element. - */ -export function downloadRootImageButton(filename, root, drawingOptions = []) { - const filetype = getFileExtensionFromName(filename); - return !isObjectOfTypeChecker(root) && h(`button.btn.download-root-image-${filetype}-button`, { - title: `Download as ${filetype.toUpperCase()}`, - onclick: async (event) => { - try { - event.target.disabled = true; - await downloadRoot(filename, root, drawingOptions); - } finally { - event.target.disabled = false; - } - }, - }, imagE()); -} diff --git a/QualityControl/public/common/downloadRootImageDropdown.js b/QualityControl/public/common/downloadRootImageDropdown.js new file mode 100644 index 000000000..4b32b46ed --- /dev/null +++ b/QualityControl/public/common/downloadRootImageDropdown.js @@ -0,0 +1,67 @@ +/** + * @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, DropdownComponent, imagE } from '/js/src/index.js'; +import { downloadRoot } from './utils.js'; +import { isObjectOfTypeChecker } from '../../library/qcObject/utils.js'; +import { RootImageDownloadExtensions } from './enums/rootImageMimes.enum.js'; + +/** + * Download root image button. + * @param {string} filename - The name of the downloaded file excluding its file extension. + * @param {RootObject} root - The JSROOT RootObject to render. + * @param {string[]} [drawingOptions=[]] - Optional array of JSROOT drawing options. + * @param {(visible: boolean) => void} [onVisibilityChange=()=>{}] - Callback for any change in + * visibility of the dropdown. + * @param {string|undefined} [uniqueIdentifier=undefined] - An unique identifier for the dropdown, + * or the `filename` if `undefined`. + * @returns {vnode|undefined} - Download root image button element. + */ +export function downloadRootImageDropdown( + filename, + root, + drawingOptions = [], + onVisibilityChange = () => {}, + uniqueIdentifier = undefined, +) { + if (isObjectOfTypeChecker(root)) { + return undefined; + } + + const dropdownComponent = DropdownComponent( + h('button.btn.save-root-as-image-button', { + title: 'Save root as image', + }, imagE()), + h('#download-root-image-dropdown', [ + RootImageDownloadExtensions() + .map((fileExtension) => h('button.btn.d-block.w-100', { + key: `${uniqueIdentifier ?? filename}.${fileExtension}`, + id: `${uniqueIdentifier ?? filename}.${fileExtension}`, + title: `Save root as image (${fileExtension})`, + onclick: async (event) => { + try { + event.target.disabled = true; + await downloadRoot(filename, fileExtension, root, drawingOptions); + } finally { + event.target.disabled = false; + dropdownComponent.state.hidePopover(); + } + }, + }, fileExtension)), + ]), + { onVisibilityChange }, + ); + + return dropdownComponent; +} diff --git a/QualityControl/public/common/enums/rootImageMimes.enum.js b/QualityControl/public/common/enums/rootImageMimes.enum.js new file mode 100644 index 000000000..e014eb8ba --- /dev/null +++ b/QualityControl/public/common/enums/rootImageMimes.enum.js @@ -0,0 +1,37 @@ +/** + * @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. + */ + +/** + * Enumeration for allowed `ROOT.makeImage` file extensions to MIME types + * @enum {string} + * @readonly + */ +export const RootImageDownloadSupportedTypes = Object.freeze({ + SVG: 'image/svg+xml', + PNG: 'file/png', + JPG: 'file/jpeg', + JPEG: 'file/jpeg', + WEBP: 'file/webp', +}); + +/** + * Get the list of unique supported ROOT image download extensions + * @returns {string[]} - Array of supported ROOT image download extensions + */ +export const RootImageDownloadExtensions = () => { + const extensions = new Set(); + Object.keys(RootImageDownloadSupportedTypes) + .forEach((ext) => extensions.add(ext.toLowerCase())); + return Array.from(extensions); +}; diff --git a/QualityControl/public/common/utils.js b/QualityControl/public/common/utils.js index d71b5a0bc..31910ed20 100644 --- a/QualityControl/public/common/utils.js +++ b/QualityControl/public/common/utils.js @@ -14,21 +14,10 @@ import { isUserRoleSufficient } from '../../../../library/userRole.enum.js'; import { generateDrawingOptionString } from '../../library/qcObject/utils.js'; +import { RootImageDownloadSupportedTypes } from './enums/rootImageMimes.enum.js'; /* global JSROOT BOOKKEEPING */ -/** - * Map of allowed `ROOT.makeImage` file extensions to MIME types - * @type {Map} - */ -const SUPPORTED_ROOT_IMAGE_FILE_TYPES = new Map([ - ['svg', 'image/svg+xml'], - ['png', 'file/png'], - ['jpg', 'file/jpeg'], - ['jpeg', 'file/jpeg'], - ['webp', 'file/webp'], -]); - /** * Generates a new ObjectId * @returns {string} 16 random chars, base 16 @@ -47,6 +36,32 @@ export function clone(obj) { return JSON.parse(JSON.stringify(obj)); } +// Map storing timers per key +const simpleDebouncerTimers = new Map(); + +/** + * Produces a debounced function that uses a key to manage timers. + * Each key has its own debounce timer, so calls with different keys + * are debounced independently. + * @template PrimitiveKey extends unknown + * @param {PrimitiveKey} key - The key for this call. + * @param {(key: PrimitiveKey) => void} fn - Function to debounce. + * @param {number} time - Debounce delay in milliseconds. + * @returns {undefined} + */ +export function simpleDebouncer(key, fn, time) { + if (simpleDebouncerTimers.has(key)) { + clearTimeout(simpleDebouncerTimers.get(key)); + } + + const timerId = setTimeout(() => { + fn(key); + simpleDebouncerTimers.delete(key); + }, time); + + simpleDebouncerTimers.set(key, timerId); +} + /** * Produces a lambda function waiting `time` ms before calling fn. * No matter how many calls are done to lambda, the last call is the waiting starting point. @@ -178,14 +193,6 @@ export const camelToTitleCase = (text) => { return titleCase; }; -/** - * Get the file extension from a filename - * @param {string} filename - The file name including the file extension - * @returns {string} - the file extension - */ -export const getFileExtensionFromName = (filename) => - filename.substring(filename.lastIndexOf('.') + 1).toLowerCase().trim(); - /** * Helper to trigger a download for a file * @param {string} url - The URL to the file source @@ -216,14 +223,14 @@ export const downloadFile = (file, filename) => { /** * Generates a rasterized image of a JSROOT RootObject and triggers download. - * @param {string} filename - The name of the downloaded file including its extension. + * @param {string} filename - The name of the downloaded file excluding the file extension. + * @param {string} filetype - The file extension of the downloaded file. * @param {RootObject} root - The JSROOT RootObject to render. * @param {string[]} [drawingOptions=[]] - Optional array of JSROOT drawing options. * @returns {undefined} */ -export const downloadRoot = async (filename, root, drawingOptions = []) => { - const filetype = getFileExtensionFromName(filename); - const mime = SUPPORTED_ROOT_IMAGE_FILE_TYPES.get(filetype); +export const downloadRoot = async (filename, filetype, root, drawingOptions = []) => { + const mime = RootImageDownloadSupportedTypes[filetype.toLocaleUpperCase()]; if (!mime) { throw new Error(`The file extension (${filetype}) is not supported`); } @@ -235,7 +242,7 @@ export const downloadRoot = async (filename, root, drawingOptions = []) => { as_buffer: true, }); const blob = new Blob([image], { type: mime }); - downloadFile(blob, filename); + downloadFile(blob, `${filename}.${filetype}`); }; /** diff --git a/QualityControl/public/layout/view/panels/objectInfoResizePanel.js b/QualityControl/public/layout/view/panels/objectInfoResizePanel.js index c4db81c7e..2707d866c 100644 --- a/QualityControl/public/layout/view/panels/objectInfoResizePanel.js +++ b/QualityControl/public/layout/view/panels/objectInfoResizePanel.js @@ -16,7 +16,7 @@ import { downloadButton } from '../../../common/downloadButton.js'; import { isOnLeftSideOfViewport } from '../../../common/utils.js'; import { defaultRowAttributes, qcObjectInfoPanel } from './../../../common/object/objectInfoCard.js'; import { h, iconResizeBoth, info } from '/js/src/index.js'; -import { downloadRootImageButton } from '../../../common/downloadRootImageButton.js'; +import { downloadRootImageDropdown } from '../../../common/downloadRootImageDropdown.js'; /** * Builds 2 actionable buttons which are to be placed on top of a JSROOT plot @@ -40,8 +40,9 @@ export const objectInfoResizePanel = (model, tabObject) => { const toUseDrawingOptions = Array.from(new Set(ignoreDefaults ? drawingOptions : [...drawingOptions, ...displayHints, ...drawOptions])); + const visibility = object.getExtraObjectData(tabObject.id)?.saveImageDropdownOpen ? 'visible' : 'hidden'; return h('.text-right.resize-element.item-action-row.flex-row.g1', { - style: 'visibility: hidden; padding: .25rem .25rem 0rem .25rem;', + style: `visibility: ${visibility}; padding: .25rem .25rem 0rem .25rem;`, }, [ h('.dropdown', { class: isSelectedOpen ? 'dropdown-open' : '', @@ -69,10 +70,14 @@ export const objectInfoResizePanel = (model, tabObject) => { ), ]), objectRemoteData.isSuccess() && [ - downloadRootImageButton( - `${objectRemoteData.payload.name}.png`, + downloadRootImageDropdown( + objectRemoteData.payload.name, objectRemoteData.payload.qcObject.root, toUseDrawingOptions, + (isDropdownOpen) => { + object.appendExtraObjectData(tabObject.id, { saveImageDropdownOpen: isDropdownOpen }); + }, + tabObject.id, ), downloadButton({ href: model.objectViewModel.getDownloadQcdbObjectUrl(objectRemoteData.payload.id), diff --git a/QualityControl/public/object/QCObject.js b/QualityControl/public/object/QCObject.js index 72780449e..659a22945 100644 --- a/QualityControl/public/object/QCObject.js +++ b/QualityControl/public/object/QCObject.js @@ -14,7 +14,7 @@ import { RemoteData, iconCaretTop, BrowserStorage } from '/js/src/index.js'; import ObjectTree from './ObjectTree.class.js'; -import { prettyFormatDate, setBrowserTabTitle } from './../common/utils.js'; +import { simpleDebouncer, prettyFormatDate, setBrowserTabTitle } from './../common/utils.js'; import { isObjectOfTypeChecker } from './../library/qcObject/utils.js'; import { BaseViewModel } from '../common/abstracts/BaseViewModel.js'; import { StorageKeysEnum } from '../common/enums/storageKeys.enum.js'; @@ -39,6 +39,7 @@ export default class QCObject extends BaseViewModel { this.selected = null; // Object - { name; createTime; lastModified; } this.selectedOpen = false; this.objects = {}; // ObjectName -> RemoteData.payload -> plot + this._extraObjectData = {}; this.searchInput = ''; // String - content of input search this.searchResult = []; // Array - result list of search @@ -303,6 +304,7 @@ export default class QCObject extends BaseViewModel { async loadObjects(objectsName) { this.objectsRemote = RemoteData.loading(); this.objects = {}; // Remove any in-memory loaded objects + this._extraObjectData = {}; // Remove any in-memory extra object data this.model.services.object.objectsLoadedMap = {}; // TODO not here this.notify(); if (!objectsName || !objectsName.length) { @@ -642,4 +644,49 @@ export default class QCObject extends BaseViewModel { } this.loadList(); } + + /** + * Returns the extra data associated with a given object name. + * @param {string} objectName The name of the object whose extra data should be retrieved. + * @returns {object | undefined} The extra data associated with the given object name, or undefined if none exists. + */ + getExtraObjectData(objectName) { + return this._extraObjectData[objectName]; + } + + /** + * Appends extra data to an existing object entry. + * Existing keys are preserved unless overwritten by the provided data. If no data exists, a new entry is created. + * @param {string} objectName The name of the object to which extra data should be appended. + * @param {object} data The extra data to merge into the existing object data. + * @returns {undefined} + */ + appendExtraObjectData(objectName, data) { + this._extraObjectData[objectName] = { ...this._extraObjectData[objectName] ?? {}, ...data }; + // debounce notify by 1ms + simpleDebouncer('QCObject.appendExtraObjectData', () => this.notify(), 1); + } + + /** + * Sets (overwrites) the extra data for a given object name. + * Any previously stored data for the object is replaced entirely. + * @param {string} objectName The name of the object whose extra data should be set. + * @param {object | undefined} data The extra data to associate with the object. + * @returns {undefined} + */ + setExtraObjectData(objectName, data) { + this._extraObjectData[objectName] = data; + // debounce notify by 1ms + simpleDebouncer('QCObject.setExtraObjectData', () => this.notify(), 1); + } + + /** + * Clears all stored extra object data. + * After calling this method, no extra data will be associated with any object name. + * @returns {undefined} + */ + clearAllExtraObjectData() { + this._extraObjectData = {}; + this.notify(); + } } diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index 7a9b19716..1a82cd81c 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -28,9 +28,9 @@ import virtualTable from './virtualTable.js'; import { defaultRowAttributes, qcObjectInfoPanel } from '../common/object/objectInfoCard.js'; import { downloadButton } from '../common/downloadButton.js'; import { resizableDivider } from '../common/resizableDivider.js'; +import { downloadRootImageDropdown } from '../common/downloadRootImageDropdown.js'; import { SortDirectionsEnum } from '../common/enums/columnSort.enum.js'; import { sortableTableHead } from '../common/sortButton.js'; -import { downloadRootImageButton } from '../common/downloadRootImageButton.js'; /** * Shows a page to explore though a tree of objects with a preview on the right if clicked @@ -120,7 +120,7 @@ const drawPlot = (model, object) => { : `?page=objectView&objectName=${name}`; return h('', { style: 'height:100%; display: flex; flex-direction: column' }, [ h('.item-action-row.flex-row.g1.p1', [ - downloadRootImageButton(`${name}.png`, root, ['stat']), + downloadRootImageDropdown(name, root, ['stat']), downloadButton({ href: model.objectViewModel.getDownloadQcdbObjectUrl(id), title: 'Download root object', diff --git a/QualityControl/public/pages/objectView/ObjectViewPage.js b/QualityControl/public/pages/objectView/ObjectViewPage.js index a7576a438..d7e076c9f 100644 --- a/QualityControl/public/pages/objectView/ObjectViewPage.js +++ b/QualityControl/public/pages/objectView/ObjectViewPage.js @@ -20,7 +20,7 @@ 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 { downloadRootImageButton } from '../../common/downloadRootImageButton.js'; +import { downloadRootImageDropdown } from '../../common/downloadRootImageDropdown.js'; /** * Shows a page to view an object on the whole page @@ -66,7 +66,7 @@ const objectPlotAndInfo = (objectViewModel) => ), ), h('.item-action-row.flex-row.g1.p2', [ - downloadRootImageButton(`${qcObject.name}.png`, qcObject.qcObject.root, drawingOptions), + downloadRootImageDropdown(qcObject.name, qcObject.qcObject.root, drawingOptions), downloadButton({ href: objectViewModel.getDownloadQcdbObjectUrl(qcObject.id), title: 'Download root object', diff --git a/QualityControl/test/lib/services/BookkeepingService.test.js b/QualityControl/test/lib/services/BookkeepingService.test.js index d60869ebd..4a81d889c 100644 --- a/QualityControl/test/lib/services/BookkeepingService.test.js +++ b/QualityControl/test/lib/services/BookkeepingService.test.js @@ -17,7 +17,9 @@ import { suite, test, before, beforeEach, afterEach } from 'node:test'; import nock from 'nock'; import { stub, restore } from 'sinon'; -import { BookkeepingService, GET_DETECTORS_PATH } from '../../../lib/services/BookkeepingService.js'; +import { + BookkeepingService, GET_BKP_GUI_STATUS_PATH, GET_DETECTORS_PATH, +} from '../../../lib/services/BookkeepingService.js'; import { RunStatus } from '../../../common/library/runStatus.enum.js'; /** @@ -153,7 +155,7 @@ export const bookkeepingServiceTestSuite = async () => { test('should return true when service responds with ok and configured', async () => { nock(VALID_CONFIG.bookkeeping.url) - .get('/api/status/database') + .get(GET_BKP_GUI_STATUS_PATH) .query({ token: VALID_CONFIG.bookkeeping.token }) .reply(200, { data: { @@ -171,7 +173,7 @@ export const bookkeepingServiceTestSuite = async () => { test('should return false when status is not ok or not configured', async () => { nock(VALID_CONFIG.bookkeeping.url) - .get('/api/status/database') + .get(GET_BKP_GUI_STATUS_PATH) .query({ token: VALID_CONFIG.bookkeeping.token }) .reply(200, { data: { @@ -188,7 +190,7 @@ export const bookkeepingServiceTestSuite = async () => { test('should return false and set error on request failure', async () => { nock(VALID_CONFIG.bookkeeping.url) - .get('/api/status/database') + .get(GET_BKP_GUI_STATUS_PATH) .query({ token: VALID_CONFIG.bookkeeping.token }) .replyWithError('connection failed'); diff --git a/QualityControl/test/public/pages/layout-list.test.js b/QualityControl/test/public/pages/layout-list.test.js index 0217fedc1..426792efd 100644 --- a/QualityControl/test/public/pages/layout-list.test.js +++ b/QualityControl/test/public/pages/layout-list.test.js @@ -49,7 +49,8 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent) await testParent.test('should go to layoutList page when clicking on title', { timeout }, async () => { await page.goto(`${url}?page=about`, { waitUntil: 'networkidle0' }); await page.click('#qcgTitle'); - await delay(2000); + await delay(300); + await page.waitForNetworkIdle({ timeout: 2000 }).catch(() => { /* ignore timeout error */ }); const location = await page.evaluate(() => window.location); strictEqual(location.search, '?page=layoutList'); }); diff --git a/QualityControl/test/public/pages/layout-show.test.js b/QualityControl/test/public/pages/layout-show.test.js index 69219abe2..53ef992ad 100644 --- a/QualityControl/test/public/pages/layout-show.test.js +++ b/QualityControl/test/public/pages/layout-show.test.js @@ -50,12 +50,12 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => ); await testParent.test( - 'should have a correctly made download root as image button', + 'should have a correctly made save root as image button', { timeout }, async () => { - const exists = await page.evaluate(() => document.querySelector('.download-root-image-png-button') !== null); + const exists = await page.evaluate(() => document.querySelector('.save-root-as-image-button') !== null); - ok(exists, 'Expected ROOT image download button to exist'); + ok(exists, 'Expected ROOT image save button to exist'); }, ); @@ -605,7 +605,11 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => const checkInvalidJSON = async (page, mockedJSON, errorMessage) => { const textareaPath = 'body > div > div > div > div > textarea'; await page.locator(textareaPath).fill(mockedJSON); - await delay(50); + await page.waitForFunction( + (error) => document.querySelector('.o2-modal-content .danger')?.textContent === error, + { timeout: 1000 }, + errorMessage, + ).catch(() => { /* ignore timeout error */ }); const [updateButtonIsDisabled, message] = await page.evaluate(() => { const updateButtonPath = 'body > div > div > div > div > button:nth-child(1)'; diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index 6f6974b69..2ca660acd 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -89,12 +89,12 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) ); await testParent.test( - 'should have a correctly made download root as image button', + 'should have a correctly made save root as image button', { timeout }, async () => { - const exists = await page.evaluate(() => document.querySelector('.download-root-image-png-button') !== null); + const exists = await page.evaluate(() => document.querySelector('.save-root-as-image-button') !== null); - ok(exists, 'Expected ROOT image download button to exist'); + ok(exists, 'Expected ROOT image save button to exist'); }, ); 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 d0e0d54f3..dc874da9d 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 @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import {strictEqual, deepStrictEqual, match, ok} from 'node:assert'; +import { strictEqual, deepStrictEqual, match, ok } from 'node:assert'; import { delay } from '../../testUtils/delay.js'; import { StorageKeysEnum } from '../../../public/common/enums/storageKeys.enum.js'; import { @@ -20,6 +20,7 @@ import { removeLocalStorage, setLocalStorageAsJson, } from '../../testUtils/localStorage.js'; +import { RootImageDownloadExtensions } from '../../../public/common/enums/rootImageMimes.enum.js'; const OBJECT_VIEW_PAGE_PARAM = '?page=objectView&objectId=123456'; @@ -103,12 +104,39 @@ export const objectViewFromLayoutShowTests = async (url, page, timeout = 5000, t ); await testParent.test( - 'should have a correctly made download root as image button', + 'should have a correctly made save root as image button', { timeout }, async () => { - const exists = await page.evaluate(() => document.querySelector('.download-root-image-png-button') !== null); + const exists = await page.evaluate(() => document.querySelector('.save-root-as-image-button') !== null); - ok(exists, 'Expected ROOT image download button to exist'); + ok(exists, 'Expected ROOT image save button to exist'); + }, + ); + + await testParent.test( + 'save root as image dropdown should have the correct filetype options', + { timeout }, + async () => { + const FILENAME = 'qc/test/object/1'; + + await page.locator('.save-root-as-image-button').click(); + await delay(100); // wait for the dropdown to appear + await page.waitForSelector('#download-root-image-dropdown', { + visible: true, + timeout: 1000, + }); + + const expectedExtensionTypes = RootImageDownloadExtensions(); + + const testedOptions = await page.evaluate(() => + Array.from(document.querySelectorAll('#download-root-image-dropdown > button')) + .map((buttonElement) => buttonElement.id)); + const expectedOptions = expectedExtensionTypes.map((filetype) => `${FILENAME}.${filetype}`); + deepStrictEqual( + testedOptions, + expectedOptions, + `Save options ${JSON.stringify(testedOptions)} should be ${JSON.stringify(expectedOptions)}`, + ); }, ); diff --git a/QualityControl/test/test-index.js b/QualityControl/test/test-index.js index bfe6f8390..3f653a7ed 100644 --- a/QualityControl/test/test-index.js +++ b/QualityControl/test/test-index.js @@ -112,7 +112,7 @@ const QC_DRAWING_OPTIONS_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 13; const LAYOUT_LIST_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 17; const OBJECT_TREE_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 20; const OBJECT_VIEW_FROM_OBJECT_TREE_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 5; -const OBJECT_VIEW_FROM_LAYOUT_SHOW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 17; +const OBJECT_VIEW_FROM_LAYOUT_SHOW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 18; const LAYOUT_SHOW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 23; const ABOUT_VIEW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 6; const FILTER_TEST_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 26; @@ -211,82 +211,82 @@ suite('All Tests - QCG', { timeout: FRONT_END_TIMEOUT + BACK_END_TIMEOUT }, asyn ); }); - // suite('API - test suite', { timeout: FRONT_END_TIMEOUT }, async () => { - // let browser = undefined; - // let subprocess = undefined; - // let subprocessOutput = undefined; - - // before(async () => { - // ({ browser, subprocess, subprocessOutput } = await setupServerForIntegrationTests()); - // }, { timeout: 5000 }); - - // after(async () => { - // await terminateSessionAndLog(browser, subprocessOutput, subprocess); - // }); - - // suite('Layout GET request test suite', async () => apiGetLayoutsTests()); - // suite('Layout PUT request test suite', async () => apiPutLayoutTests()); - // suite('Layout PATCH request test suite', async () => apiPatchLayoutTests()); - // suite('Object GET request test suite', async () => apiGetObjectsTests()); - // suite('Filters GET run status test suite', async () => await apiGetRunStatusTests()); - // }); - - // suite('Back-end test suite', { timeout: BACK_END_TIMEOUT }, async () => { - // suite('Lib - Test Suite', async () => { - // suite('Utility "errorHandler" methods test suite', async () => await errorHandlerTestSuite()); - // suite('Utility "httpRequests" methods test suite', async () => await httpRequestsTestSuite()); - // suite('Layout Utils - calculateLabelsForLayout test suite', () => addLabelsToLayoutTestSuite()); - // suite('Layout Utils - trimLayoutPerRequiredFields test suite', () => trimLayoutPerRequiredFieldsTestSuite()); - // }); - - // suite('Common Library - Test Suite', () => { - // suite('CL - Object Utility methods test suite', () => commonLibraryQcObjectUtilsTestSuite()); - // suite('CL - DateTime Utility methods test suite', () => commonLibraryUtilsDateTimeTestSuite()); - // }); - - // suite('Repositories - Test Suite', async () => { - // suite('Base Repository - Test Suite', async () => await baseRepositoryTestSuite()); - // suite('Layout Repository - Database Test Suite', async () => await layoutRepositoryTestSuite()); - // suite('Layout Repository - Test Suite', async () => await layoutRepositoryTest()); - // suite('User Repository - Test Suite', async () => await userRepositoryTestSuite()); - // suite('Chart Repository - Test Suite', async () => await chartRepositoryTestSuite()); - // suite('Chart Options Repository - Test Suite', async () => await chartOptionsRepositoryTestSuite()); - // suite('Grid Tab Cell Repository - Test Suite', async () => await gridTabCellRepositoryTestSuite()); - // suite('Tab Repository - Test Suite', async () => await tabRepositoryTestSuite()); - // suite('Option Repository - Test Suite', async () => await optionRepositoryTestSuite()); - // }); - - // suite('Services - Test Suite', async () => { - // suite('CcdbService - Test Suite', async () => await ccdbServiceTestSuite()); - // suite('QcdbDownloadService - Test Suite', async () => await qcdbDownloadServiceTestSuite()); - // suite('StatusService - Test Suite', async () => await statusServiceTestSuite()); - // suite('JsonServiceTest test suite', async () => await jsonFileServiceTestSuite()); - // suite('FilterService', async () => await filterServiceTestSuite()); - // suite('RunModeService - Test Suite', async () => await runModeServiceTestSuite()); - // suite('QcObjectService - Test Suite', async () => await qcObjectServiceTestSuite()); - // suite('BookkeepingServiceTest test suite', async () => await bookkeepingServiceTestSuite()); - // suite('AliEcsSynchronizer - Test Suite', async () => await aliecsSynchronizerTestSuite()); - // }); - - // suite('Middleware - Test Suite', async () => { - // suite('LayoutServiceMiddleware test suite', async () => layoutServiceMiddlewareTest()); - // suite('LayoutIdMiddleware test suite', async () => layoutIdMiddlewareTest()); - // suite('LayoutOwnerMiddleware test suite', async () => layoutOwnerMiddlewareTest()); - // suite('StatusComponentMiddleware test suite', async () => statusComponentMiddlewareTest()); - // suite('RunModeMiddleware test suite', async () => runModeMiddlewareTest()); - // suite('RunStatusFilterMiddleware test suite', async () => runStatusFilterMiddlewareTest()); - // suite('ObjectsGetValidationMiddleware test suite', async () => objectsGetValidationMiddlewareTest()); - // suite('ObjectGetContentsValidationMiddleware test suite', async () => - // objectGetContentsValidationMiddlewareTest()); - // suite('ObjectGetByIdValidationMiddleware test suite', async () => objectGetByIdValidationMiddlewareTest()); - // }); - - // suite('Controllers - Test Suite', async () => { - // suite('LayoutController test suite', async () => await layoutControllerTestSuite()); - // suite('StatusController test suite', async () => await statusControllerTestSuite()); - // suite('ObjectController test suite', async () => await objectControllerTestSuite()); - // suite('UserController - Test Suite', async () => await userControllerTestSuite()); - // suite('FiltersController test suite', async () => await filtersControllerTestSuite()); - // }); - // }); + suite('API - test suite', { timeout: FRONT_END_TIMEOUT }, async () => { + let browser = undefined; + let subprocess = undefined; + let subprocessOutput = undefined; + + before(async () => { + ({ browser, subprocess, subprocessOutput } = await setupServerForIntegrationTests()); + }, { timeout: 5000 }); + + after(async () => { + await terminateSessionAndLog(browser, subprocessOutput, subprocess); + }); + + suite('Layout GET request test suite', async () => apiGetLayoutsTests()); + suite('Layout PUT request test suite', async () => apiPutLayoutTests()); + suite('Layout PATCH request test suite', async () => apiPatchLayoutTests()); + suite('Object GET request test suite', async () => apiGetObjectsTests()); + suite('Filters GET run status test suite', async () => await apiGetRunStatusTests()); + }); + + suite('Back-end test suite', { timeout: BACK_END_TIMEOUT }, async () => { + suite('Lib - Test Suite', async () => { + suite('Utility "errorHandler" methods test suite', async () => await errorHandlerTestSuite()); + suite('Utility "httpRequests" methods test suite', async () => await httpRequestsTestSuite()); + suite('Layout Utils - calculateLabelsForLayout test suite', () => addLabelsToLayoutTestSuite()); + suite('Layout Utils - trimLayoutPerRequiredFields test suite', () => trimLayoutPerRequiredFieldsTestSuite()); + }); + + suite('Common Library - Test Suite', () => { + suite('CL - Object Utility methods test suite', () => commonLibraryQcObjectUtilsTestSuite()); + suite('CL - DateTime Utility methods test suite', () => commonLibraryUtilsDateTimeTestSuite()); + }); + + suite('Repositories - Test Suite', async () => { + suite('Base Repository - Test Suite', async () => await baseRepositoryTestSuite()); + suite('Layout Repository - Database Test Suite', async () => await layoutRepositoryTestSuite()); + suite('Layout Repository - Test Suite', async () => await layoutRepositoryTest()); + suite('User Repository - Test Suite', async () => await userRepositoryTestSuite()); + suite('Chart Repository - Test Suite', async () => await chartRepositoryTestSuite()); + suite('Chart Options Repository - Test Suite', async () => await chartOptionsRepositoryTestSuite()); + suite('Grid Tab Cell Repository - Test Suite', async () => await gridTabCellRepositoryTestSuite()); + suite('Tab Repository - Test Suite', async () => await tabRepositoryTestSuite()); + suite('Option Repository - Test Suite', async () => await optionRepositoryTestSuite()); + }); + + suite('Services - Test Suite', async () => { + suite('CcdbService - Test Suite', async () => await ccdbServiceTestSuite()); + suite('QcdbDownloadService - Test Suite', async () => await qcdbDownloadServiceTestSuite()); + suite('StatusService - Test Suite', async () => await statusServiceTestSuite()); + suite('JsonServiceTest test suite', async () => await jsonFileServiceTestSuite()); + suite('FilterService', async () => await filterServiceTestSuite()); + suite('RunModeService - Test Suite', async () => await runModeServiceTestSuite()); + suite('QcObjectService - Test Suite', async () => await qcObjectServiceTestSuite()); + suite('BookkeepingServiceTest test suite', async () => await bookkeepingServiceTestSuite()); + suite('AliEcsSynchronizer - Test Suite', async () => await aliecsSynchronizerTestSuite()); + }); + + suite('Middleware - Test Suite', async () => { + suite('LayoutServiceMiddleware test suite', async () => layoutServiceMiddlewareTest()); + suite('LayoutIdMiddleware test suite', async () => layoutIdMiddlewareTest()); + suite('LayoutOwnerMiddleware test suite', async () => layoutOwnerMiddlewareTest()); + suite('StatusComponentMiddleware test suite', async () => statusComponentMiddlewareTest()); + suite('RunModeMiddleware test suite', async () => runModeMiddlewareTest()); + suite('RunStatusFilterMiddleware test suite', async () => runStatusFilterMiddlewareTest()); + suite('ObjectsGetValidationMiddleware test suite', async () => objectsGetValidationMiddlewareTest()); + suite('ObjectGetContentsValidationMiddleware test suite', async () => + objectGetContentsValidationMiddlewareTest()); + suite('ObjectGetByIdValidationMiddleware test suite', async () => objectGetByIdValidationMiddlewareTest()); + }); + + suite('Controllers - Test Suite', async () => { + suite('LayoutController test suite', async () => await layoutControllerTestSuite()); + suite('StatusController test suite', async () => await statusControllerTestSuite()); + suite('ObjectController test suite', async () => await objectControllerTestSuite()); + suite('UserController - Test Suite', async () => await userControllerTestSuite()); + suite('FiltersController test suite', async () => await filtersControllerTestSuite()); + }); + }); });