diff --git a/QualityControl/public/Model.js b/QualityControl/public/Model.js index b73cb058c..98a02da9f 100644 --- a/QualityControl/public/Model.js +++ b/QualityControl/public/Model.js @@ -75,6 +75,7 @@ export default class Model extends Observable { this.accountMenuEnabled = false; this.page = null; this._isImportVisible = false; // Visibility of modal allowing user to import a layout as JSON + this._isUpdateVisible = false; // Visibility of modal allowing user to edit JSON of an existing layout // Setup router this.router = new QueryRouter(); @@ -374,4 +375,22 @@ export default class Model extends Observable { this._isImportVisible = value ? true : false; this.notify(); } + + /** + * Returns the visibility of the edit JSON layout modal + * @returns {boolean} - whether import modal is visible + */ + get isUpdateVisible() { + return this._isUpdateVisible; + } + + /** + * Sets the visibility of the edit JSON layout modal + * @param {boolean} value - value to be set for modal visibility + * @returns {undefined} + */ + set isUpdateVisible(value) { + this._isUpdateVisible = value ? true : false; + this.notify(); + } } diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index 6680401c3..9ede3bab5 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -62,3 +62,6 @@ .o2-modal{z-index:1001; display:block; padding-top:10em; position:fixed; left:0; top:0; width:100%; height:100%; overflow:auto; background-color:rgb(0,0,0); background-color:rgba(0,0,0,0.4); } .o2-modal-content{margin:auto; background-color:#fff; position:relative; padding:0; outline:0; width:700px; border-radius: 5px 20px 5px; } + +.right-menu {right:0; left:auto;} +.resize-vertical { resize: vertical} diff --git a/QualityControl/public/layout/Layout.js b/QualityControl/public/layout/Layout.js index 857439db8..735467297 100644 --- a/QualityControl/public/layout/Layout.js +++ b/QualityControl/public/layout/Layout.js @@ -43,6 +43,7 @@ export default class Layout extends Observable { this.tabInterval = undefined; // JS Interval to change currently displayed tab this.newJSON = undefined; + this.updatedJSON = undefined; this.requestedLayout = RemoteData.notAsked(); @@ -52,6 +53,8 @@ export default class Layout extends Observable { this.editingTabObject = null; // Pointer to a tabObject being modified this.editOriginalClone = null; // Contains a deep clone of item before editing + this.isEditLayoutDropdownOpen = false; + // https://github.com/hootsuite/grid this.gridListSize = 3; @@ -313,6 +316,15 @@ export default class Layout extends Observable { this.model.notify(); } + /** + * Toggle edit menu dropdown + * @returns {undefined} + */ + async toggleEditMenu() { + this.isEditLayoutDropdownOpen = !this.isEditLayoutDropdownOpen; + this.notify(); + } + /** * Method to allow more than 3x3 grid * @param {string} value - of grid resize @@ -450,6 +462,7 @@ export default class Layout extends Observable { * @returns {undefined} */ edit() { + this.toggleEditMenu(); this.model.services.object.listObjects(); if (!this.item) { throw new Error('An item should be loaded before editing it'); @@ -727,4 +740,72 @@ export default class Layout extends Observable { this.selectTab(this._tabIndex); } } + + /** + * Validates the provided layout and updates the layout state accordingly. + * Used by the textarea input to check the JSON structure on each input change. + * @param {string} newLayout - The layout to check. + * @returns {undefined} + */ + checkLayoutToUpdate(newLayout) { + try { + const newJSON = JSON.parse(newLayout); + this.checkForManualIdEntry(newJSON); + this.model.services.layout.update = RemoteData.success(); + } catch (error) { + this.model.services.layout.update = RemoteData.failure(error.message || error); + } + this.updatedJSON = newLayout; + this.notify(); + } + + /** + * Checks that user doesn't enter the ID + * @param {object} layoutJSON - layout entered by the user in the box + * @returns {undefined} + */ + checkForManualIdEntry(layoutJSON) { + if (Object.keys(layoutJSON).includes('id')) { + throw new Error('Error: Manual entry of an ID is not allowed, as it is automatically assigned by the system.'); + } + } + + /** + * Updates the layout by parsing the updated JSON and saving the layout state. + * @returns {undefined} + */ + updateLayout() { + try { + const updatedLayout = LayoutUtils.fromSkeleton({ + ...this.item, + ...JSON.parse(this.updatedJSON), + }); + + this.item = { + ...updatedLayout, + id: this.item.id, + }; + + this.save(); + this.updatedJSON = undefined; + this.model.isUpdateVisible = !this.model.isUpdateVisible; + } catch (error) { + this.changeUpdateStatus(RemoteData.failure(error.message || error)); + } + this.notify(); + } + + /** + * Method to initialize the status of the EDIT as JSON modal + * Sets the layout skeleton to the current layout + * Sets the error message to null + * Sets the visibility of the model to true + * @returns {undefined} + */ + initializeEditViaJson() { + this.model.services.layout.update = RemoteData.success(); + this.updatedJSON = LayoutUtils.toSkeleton(this.item); + this.model.isUpdateVisible = true; + this.toggleEditMenu(); + } } diff --git a/QualityControl/public/layout/panels/editModal.js b/QualityControl/public/layout/panels/editModal.js new file mode 100644 index 000000000..96892aaa4 --- /dev/null +++ b/QualityControl/public/layout/panels/editModal.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 } from '/js/src/index.js'; + +/** + * Displays a panel allowing users to edit the JSON file of the layout + * @param {Model} model - root model of the application + * @returns {vnode} - virtual node element + */ +export default (model) => h('.o2-modal', [ + h('.o2-modal-content', [ + h('.p2.text-center.flex-column', [ + h('h4.pv1', 'Edit JSON file of a layout'), + h('', h('textarea.form-control.w-100.resize-vertical', { + rows: 15, + oninput: (e) => model.layout.checkLayoutToUpdate(e.target.value), + id: 'layout-json-editor', + value: model.layout.updatedJSON, + })), + model.services.layout.update.match({ + NotAsked: () => null, + Loading: () => h('', 'Loading...'), + Success: (_) => null, + Failure: (error) => h('.danger.pv1', error), + }), + h('.btn-group.w-100.align-center.pv1', { + style: 'display:flex; justify-content:center;', + }, [ + h('button.btn.btn-primary', { + disabled: model.services.layout.update.isFailure(), + onclick: () => model.layout.updateLayout(), + }, 'Update layout'), + h('button.btn', { + onclick: () => { + model.isUpdateVisible = false; + }, + }, 'Cancel'), + ]), + ]), + ]), +]); diff --git a/QualityControl/public/layout/panels/importModal.js b/QualityControl/public/layout/panels/importModal.js index a72ba225d..6b6e60393 100644 --- a/QualityControl/public/layout/panels/importModal.js +++ b/QualityControl/public/layout/panels/importModal.js @@ -25,12 +25,11 @@ export default (model) => h('.o2-modal-content', [ h('.p2.text-center.flex-column', [ h('h4.pv1', 'Import a layout in JSON format'), - h('', h('textarea.form-control.w-100', { + h('', h('textarea.form-control.w-100.resize-vertical', { rows: 15, placeholder: 'e.g.\n{\n\t"name": "my layout",\n\t"displayTimestamp": "false",' + '\n\t"displayTimestamp": "autoTabChange": "10", \n\t"tabs": "[]"\n}', oninput: (e) => model.layout.setImportValue(e.target.value), - style: 'resize: vertical;', })), model.services.layout.new.match({ NotAsked: () => null, diff --git a/QualityControl/public/layout/view/header.js b/QualityControl/public/layout/view/header.js index 608dedb9d..515988ef5 100644 --- a/QualityControl/public/layout/view/header.js +++ b/QualityControl/public/layout/view/header.js @@ -61,10 +61,21 @@ const toolbarViewMode = (model) => { download: `layout-${layoutItem.name}-skeleton.json`, }, iconShareBoxed()), model.session.personid == layoutItem.owner_id && [ - h('button.btn.btn-primary', { - onclick: () => model.layout.edit(), + h('.dropdown', { title: 'Edit layout', - }, iconPencil()), + class: model.layout.isEditLayoutDropdownOpen ? 'dropdown-open' : '', + }, [ + h('button.btn.btn-primary', { onclick: () => model.layout.toggleEditMenu() }, iconPencil()), + h('.dropdown-menu.right-menu', [ + h('.text-ellipsis', [ + h('a.menu-item', { title: 'Edit via GUI', onclick: () => model.layout.edit() }, 'Edit via GUI'), + h('a.menu-item', { + title: 'Edit via JSON', + onclick: () => model.layout.initializeEditViaJson(), + }, 'Edit via JSON'), + ]), + ]), + ]), h('button.btn.btn-danger', { onclick: () => confirm('Are you sure to delete this layout?') && model.layout.deleteItem(), title: 'Delete layout', diff --git a/QualityControl/public/services/Layout.service.js b/QualityControl/public/services/Layout.service.js index ceec4364c..4b95e3962 100644 --- a/QualityControl/public/services/Layout.service.js +++ b/QualityControl/public/services/Layout.service.js @@ -30,6 +30,7 @@ export default class LayoutService { this.loader = model.loader; this.new = RemoteData.notAsked(); // RemoteData for creating a new layout via modal of import or prompt + this.update = RemoteData.notAsked(); // RemoteData for updating the JSON file that builds the layout this.list = RemoteData.notAsked(); // List of all existing layouts in QCG; this.userList = RemoteData.notAsked(); // List of layouts owned by current user; diff --git a/QualityControl/public/view.js b/QualityControl/public/view.js index ec1b1d1a9..b91319770 100644 --- a/QualityControl/public/view.js +++ b/QualityControl/public/view.js @@ -20,6 +20,8 @@ import header from './common/header.js'; import layoutListPage from './layout/list/page.js'; import layoutViewPage from './layout/view/page.js'; import layoutImportModal from './layout/panels/importModal.js'; +import layoutEditModal from './layout/panels/editModal.js'; + import objectTreePage from './object/objectTreePage.js'; import ObjectViewPage from './pages/objectView/ObjectViewPage.js'; import frameworkInfoPage from './frameworkInfo/frameworkInfoPage.js'; @@ -30,6 +32,7 @@ import frameworkInfoPage from './frameworkInfo/frameworkInfoPage.js'; * @returns {vnode} - virtual node element */ export default (model) => [ + model.isUpdateVisible && layoutEditModal(model), model.isImportVisible && layoutImportModal(model), model.page === 'objectView' ? ObjectViewPage(model) : h('.absolute-fill.flex-column', [ diff --git a/QualityControl/test/public/pages/layout-show.test.js b/QualityControl/test/public/pages/layout-show.test.js index bb8d42a8e..cfeb017c9 100644 --- a/QualityControl/test/public/pages/layout-show.test.js +++ b/QualityControl/test/public/pages/layout-show.test.js @@ -13,6 +13,7 @@ import { strictEqual, ok, deepStrictEqual } from 'node:assert'; import { delay } from '../../testUtils/delay.js'; +import { editedMockedLayout } from '../../setup/seeders/layout-show/json-file-mock.js'; /** * Performs a series of automated tests on the layoutShow page using Puppeteer. @@ -159,12 +160,21 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => ); await testParent.test( - 'should have one edit button in the header to go in edit mode', + 'should have two options for editing the layout', { timeout }, async () => { - const buttonPath = 'header > div > div:nth-child(3) > div > button:nth-child(3)'; - const editButton = await page.evaluate((buttonPath) => document.querySelector(buttonPath).title, buttonPath); - strictEqual(editButton, 'Edit layout'); + const editButtonPath = 'header > div > div:nth-child(3) > div > div > button'; + await page.locator(editButtonPath).click(); + const titles = await page.evaluate(() => { + const firstLinkPath = 'header > div > div:nth-child(3) > div > div > div > div > a:nth-child(1)'; + const secondLinkPath = 'header > div > div:nth-child(3) > div > div > div > div > a:nth-child(2)'; + const firstLinkTitle = document.querySelector(firstLinkPath).title; + const secondLinkTitle = document.querySelector(secondLinkPath).title; + return [firstLinkTitle, secondLinkTitle]; + }); + + strictEqual(titles[0], 'Edit via GUI'); + strictEqual(titles[1], 'Edit via JSON'); }, ); @@ -172,8 +182,8 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => 'should click the edit button in the header and enter edit mode', { timeout }, async () => { - const editButtonPath = 'header > div > div:nth-child(3) > div > button:nth-child(3)'; - await page.locator(editButtonPath).click(); + const editViaGUIButtonPath = 'header > div > div:nth-child(3) > div > div > div > div > a:nth-child(1)'; + await page.locator(editViaGUIButtonPath).click(); }, ); @@ -248,4 +258,133 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => await page.waitForSelector('nav .menu-title', { timeout: 5000 }); }, ); + + await testParent.test( + 'should open JSON editor when clicking "Edit via JSON"', + { timeout }, + async () => { + const editDropdownButtonPath = 'header > div > div:nth-child(3) > div > div > button'; + const editViaJSONButtonPath = 'header > div > div:nth-child(3) > div > div > div > div > a:nth-child(2)'; + await page.locator(editDropdownButtonPath).click(); + await delay(100); + await page.locator(editViaJSONButtonPath).click(); + await delay(100); + const result = await page.evaluate(() => { + const titlePath = 'body > div > div > div > h4'; + const textareaPath = 'body > div > div > div > div > textarea'; + const updateButtonPath = 'body > div > div > div > div > button:nth-child(1)'; + const cancelButtonPath = 'body > div > div > div > div > button:nth-child(2)'; + const title = document.querySelector(titlePath).textContent; + const textAreaId = document.querySelector(textareaPath).id; + const updateButtonTitle = document.querySelector(updateButtonPath).textContent; + const cancelButtonTitle = document.querySelector(cancelButtonPath).textContent; + return { title, textAreaId, updateButtonTitle, cancelButtonTitle }; + }); + + strictEqual(result.title, 'Edit JSON file of a layout'); + strictEqual(result.textAreaId, 'layout-json-editor'); + strictEqual(result.updateButtonTitle, 'Update layout'); + strictEqual(result.cancelButtonTitle, 'Cancel'); + }, + ); + + await testParent.test( + 'should not have ID as an editable key in the JSON editor', + { timeout }, + async () => { + const result = await page.evaluate(() => { + const textareaPath = 'body > div > div > div > div > textarea'; + const textAreaValue = document.querySelector(textareaPath).value; + const json = JSON.parse(textAreaValue); + const jsonDoesNotContainId = !Object.prototype.hasOwnProperty.call(json, 'id'); + return jsonDoesNotContainId; + }); + strictEqual(result, true); + }, + ); + + await testParent.test( + 'should disable the "Update layout" button when JSON has an "id" key and display error message', + { timeout }, + async () => { + const mockJSONWithId = '{ "id" : "test" }'; + const expectedErrorMessage = + 'Error: Manual entry of an ID is not allowed, as it is automatically assigned by the system.'; + + await checkInvalidJSON(page, mockJSONWithId, expectedErrorMessage); + }, + ); + + await testParent.test( + 'should disable the "Update layout" button when JSON is invalid and display error message', + { timeout }, + async () => { + const mockJSONInvalid = '{ "name" : "test" '; + const expectedErrorMessage = + 'Expected \',\' or \'}\' after property value in JSON at position 18 (line 1 column 19)'; + + await checkInvalidJSON(page, mockJSONInvalid, expectedErrorMessage); + }, + ); + + await testParent.test( + 'should close JSON editor when clicking "Cancel"', + { timeout }, + async () => { + const cancelButtonPath = 'body > div > div > div > div > button:nth-child(2)'; + await page.locator(cancelButtonPath).click(); + await delay(50); + const childrenCount = await page.evaluate(() => { + const bodyPath = 'body'; + const body = document.querySelector(bodyPath); + return body.children.length; + }); + strictEqual(childrenCount, 2); + }, + ); + + await testParent.test( + 'should update layout when clicking "Update layout"', + { timeout }, + async () => { + const pencilButtonPath = 'header > div > div:nth-child(3) > div > div > button'; + await page.locator(pencilButtonPath).click(); + const editViaJSONButtonPath = + 'header > div > div:nth-child(3) > div > div > div > div > a:nth-child(2)'; + page.locator(editViaJSONButtonPath).click(); + + const textareaPath = 'body > div > div > div > div > textarea'; + const mockedJSON = JSON.stringify(editedMockedLayout); + await page.locator(textareaPath).fill(mockedJSON); + + const updateButtonPath = 'body > div > div > div > div > button:nth-child(1)'; + await page.locator(updateButtonPath).click(); + await delay(50); + + const buttonsPath = 'header > div > div:nth-child(2) > div > button'; + const result = await page.evaluate((buttonsPath) => { + const tabs = document.querySelectorAll(buttonsPath); + return tabs.length === 3 && tabs[2].textContent === 'test'; + }, buttonsPath); + + strictEqual(result, true); + }, + ); +}; + +const checkInvalidJSON = async (page, mockedJSON, errorMessage) => { + const textareaPath = 'body > div > div > div > div > textarea'; + await page.locator(textareaPath).fill(mockedJSON); + await delay(50); + + const [updateButtonIsDisabled, message] = await page.evaluate(() => { + const updateButtonPath = 'body > div > div > div > div > button:nth-child(1)'; + const updateButton = document.querySelector(updateButtonPath); + const errorTextPath = 'body > div:nth-child(1) > div > div > div:nth-child(3)'; + const errorText = document.querySelector(errorTextPath).textContent; + return [updateButton.disabled, errorText]; + }); + + strictEqual(updateButtonIsDisabled, true); + strictEqual(message, errorMessage); }; diff --git a/QualityControl/test/setup/seeders/layout-show/json-file-mock.js b/QualityControl/test/setup/seeders/layout-show/json-file-mock.js new file mode 100644 index 000000000..1a418699c --- /dev/null +++ b/QualityControl/test/setup/seeders/layout-show/json-file-mock.js @@ -0,0 +1,32 @@ +export const editedMockedLayout = { + name: 'a-test', + tabs: [ + { + name: 'main', + objects: [ + { + x: 0, + y: 0, + h: 1, + w: 1, + name: 'qc/test/object/1', + options: [], + ignoreDefaults: false, + }, + ], + columns: 2, + }, + { + name: 'a', + objects: [], + columns: 2, + }, + { + name: 'test', + objects: [], + columns: 3, + }, + ], + displayTimestamp: false, + autoTabChange: 0, +}; diff --git a/QualityControl/test/setup/seeders/qcg-mock-data-template.json b/QualityControl/test/setup/seeders/qcg-mock-data-template.json index 483d7c2db..c89d2cda4 100644 --- a/QualityControl/test/setup/seeders/qcg-mock-data-template.json +++ b/QualityControl/test/setup/seeders/qcg-mock-data-template.json @@ -1,139 +1,139 @@ { - "layouts": [ - { - "id": "671b8c22402408122e2f20dd", - "name": "test", - "owner_id": 0, - "owner_name": "Anonymous", - "description": "", - "displayTimestamp": false, - "autoTabChange": 0, - "tabs": [ - { - "id": "671b8c227b3227b0c603c29d", - "name": "main", - "objects": [ - { - "id": "671b8c25d5b49dbf80e81926", - "x": 0, - "y": 0, - "h": 1, - "w": 1, - "name": "qc/MCH/QO/Aggregator/MCHQuality", - "options": [], - "autoSize": false, - "ignoreDefaults": false - }, - { - "id": "671b8c256cdd70443c1cd709", - "x": 1, - "y": 0, - "h": 1, - "w": 1, - "name": "qc/MCH/QO/DataDecodingCheck", - "options": [], - "autoSize": false, - "ignoreDefaults": false - }, - { - "id": "671b8c266dd77d73874f4e90", - "x": 2, - "y": 0, - "h": 1, - "w": 1, - "name": "qc/MCH/QO/MFTRefCheck", - "options": [], - "autoSize": false, - "ignoreDefaults": false - }, - { - "id": "671b8c2bcc75ce6053c67874", - "x": 0, - "y": 1, - "h": 1, - "w": 1, - "name": "qc/MCH/MO/Pedestals/ST5/DE1006/BadChannels_XY_B_1006", - "options": [], - "autoSize": false, - "ignoreDefaults": false - } - ], - "columns": 2 - }, - { - "id": "671b8c5aa66868891b977311", - "name": "test-tab", - "objects": [ - { - "id": "671b8c604deeb0f548863a8c", - "x": 0, - "y": 0, - "h": 1, - "w": 1, - "name": "qc/MCH/MO/Pedestals/BadChannelsPerDE", - "options": [], - "autoSize": false, - "ignoreDefaults": false - } - ], - "columns": 3 - } - ], - "collaborators": [] - }, - { - "id": "671b95883d23cd0d67bdc787", - "name": "a-test", - "owner_id": 0, - "owner_name": "Anonymous", - "description": "", - "displayTimestamp": false, - "autoTabChange": 0, - "tabs": [ - { - "id": "671b95884312f03458f1d9ca", - "name": "main", - "objects": [ - { - "id": "6724a6bd1b2bad3d713cc4ee", - "x": 0, - "y": 0, - "h": 1, - "w": 1, - "name": "qc/test/object/1", - "options": [], - "autoSize": false, - "ignoreDefaults": false - }, - { - "id": "6724a6bd1b2bad3d713cc4ee", - "x": 0, - "y": 0, - "h": 1, - "w": 1, - "name": "qc/test/object/1", - "options": [], - "autoSize": false, - "ignoreDefaults": false - } - ], - "columns": 2 - }, - { - "id": "671b958b8a5cfb52ee9ef2a1", - "name": "a", - "objects": [], - "columns": 2 - } - ], - "collaborators": [] - } - ], - "users": [ - { - "id": 0, - "name": "Anonymous", - "username": "anonymous" - } - ] -} \ No newline at end of file + "layouts": [ + { + "id": "671b8c22402408122e2f20dd", + "name": "test", + "owner_id": 0, + "owner_name": "Anonymous", + "description": "", + "displayTimestamp": false, + "autoTabChange": 0, + "tabs": [ + { + "id": "671b8c227b3227b0c603c29d", + "name": "main", + "objects": [ + { + "id": "671b8c25d5b49dbf80e81926", + "x": 0, + "y": 0, + "h": 1, + "w": 1, + "name": "qc/MCH/QO/Aggregator/MCHQuality", + "options": [], + "autoSize": false, + "ignoreDefaults": false + }, + { + "id": "671b8c256cdd70443c1cd709", + "x": 1, + "y": 0, + "h": 1, + "w": 1, + "name": "qc/MCH/QO/DataDecodingCheck", + "options": [], + "autoSize": false, + "ignoreDefaults": false + }, + { + "id": "671b8c266dd77d73874f4e90", + "x": 2, + "y": 0, + "h": 1, + "w": 1, + "name": "qc/MCH/QO/MFTRefCheck", + "options": [], + "autoSize": false, + "ignoreDefaults": false + }, + { + "id": "671b8c2bcc75ce6053c67874", + "x": 0, + "y": 1, + "h": 1, + "w": 1, + "name": "qc/MCH/MO/Pedestals/ST5/DE1006/BadChannels_XY_B_1006", + "options": [], + "autoSize": false, + "ignoreDefaults": false + } + ], + "columns": 2 + }, + { + "id": "671b8c5aa66868891b977311", + "name": "test-tab", + "objects": [ + { + "id": "671b8c604deeb0f548863a8c", + "x": 0, + "y": 0, + "h": 1, + "w": 1, + "name": "qc/MCH/MO/Pedestals/BadChannelsPerDE", + "options": [], + "autoSize": false, + "ignoreDefaults": false + } + ], + "columns": 3 + } + ], + "collaborators": [] + }, + { + "id": "671b95883d23cd0d67bdc787", + "name": "a-test", + "owner_id": 0, + "owner_name": "Anonymous", + "description": "", + "displayTimestamp": false, + "autoTabChange": 0, + "tabs": [ + { + "id": "671b95884312f03458f1d9ca", + "name": "main", + "objects": [ + { + "id": "6724a6bd1b2bad3d713cc4ee", + "x": 0, + "y": 0, + "h": 1, + "w": 1, + "name": "qc/test/object/1", + "options": [], + "autoSize": false, + "ignoreDefaults": false + }, + { + "id": "6724a6bd1b2bad3d713cc4ee", + "x": 0, + "y": 0, + "h": 1, + "w": 1, + "name": "qc/test/object/1", + "options": [], + "autoSize": false, + "ignoreDefaults": false + } + ], + "columns": 2 + }, + { + "id": "671b958b8a5cfb52ee9ef2a1", + "name": "a", + "objects": [], + "columns": 2 + } + ], + "collaborators": [] + } + ], + "users": [ + { + "id": 0, + "name": "Anonymous", + "username": "anonymous" + } + ] + } diff --git a/QualityControl/test/test-index.js b/QualityControl/test/test-index.js index e8f0a9424..c470ba474 100644 --- a/QualityControl/test/test-index.js +++ b/QualityControl/test/test-index.js @@ -34,6 +34,7 @@ import { layoutListPageTests } from './public/pages/layout-list.test.js'; import { objectTreePageTests } from './public/pages/object-tree.test.js'; import { objectViewFromObjectTreeTests } from './public/pages/object-view-from-object-tree.test.js'; import { objectViewFromLayoutShowTests } from './public/pages/object-view-from-layout-show.test.js'; +import { layoutShowTests } from './public/pages/layout-show.test.js'; /** * Backend tests imports @@ -55,7 +56,6 @@ import { statusServiceTestSuite } from './lib/services/StatusService.test.js'; import { commonLibraryQcObjectUtilsTestSuite } from './common/library/qcObject/utils.test.js'; import { commonLibraryUtilsDateTimeTestSuite } from './common/library/utils/dateTimeFormat.test.js'; -import { layoutShowTests } from './public/pages/layout-show.test.js'; const FRONT_END_PER_TEST_TIMEOUT = 5000; // each front-end test is allowed this timeout // remaining tests are based on the number of individual tests in each suite