From cf8e138ff1bb25f0dcde05d8276213e3edb298f2 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 12:56:05 +0100 Subject: [PATCH 01/10] Extract trim and add label dynamically functionality to layouts --- .../lib/repositories/LayoutRepository.js | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/QualityControl/lib/repositories/LayoutRepository.js b/QualityControl/lib/repositories/LayoutRepository.js index 266245016..3c342f476 100644 --- a/QualityControl/lib/repositories/LayoutRepository.js +++ b/QualityControl/lib/repositories/LayoutRepository.js @@ -26,20 +26,63 @@ export class LayoutRepository extends BaseRepository { * @param {object} [options.filter] - Filter layouts by containing filter.objectPath, case insensitive * @returns {Array} Array of layout objects matching the filters, containing only the specified fields */ - listLayouts({ name, fields = [], filter } = {}) { + listLayouts({ name, fields, filter } = {}) { const { layouts } = this._jsonFileService.data; const filteredLayouts = this._filterLayouts(layouts, { ...filter, name }); + const trimmedAndLabelledLayouts = filteredLayouts + .map((layout) => this._trimAndLabelLayout(layout, fields)); + return trimmedAndLabelledLayouts; + } + + /** + * Adds labels field to a layout based on contained objects in the layout tabs. + * Then, trims a layout object to only include requested fields and adds labels + * @param {object} layout - layout object to be processed + * @param {Array} fields - Array of field names to include in the returned layout object + * @returns {object} - Processed layout object with added labels and trimmed fields + */ + _trimAndLabelLayout(layout, fields) { + const labeledLayout = this._addLabelsToLayout(layout); + const trimmedLayout = this._trimLayout(labeledLayout, fields); + return trimmedLayout; + } + + /** + * Trims a layout object to only include requested fields + * @param {object} layout - layout object to be trimmed + * @param {Array} [fields = []] - Array of field names to include in the returned layout object + * @returns {object} - Trimmed layout object + */ + _trimLayout(layout, fields = []) { if (fields.length === 0) { - return filteredLayouts; + return layout; } - return filteredLayouts.map((layout) => { - const layoutObj = {}; - fields.forEach((field) => { - layoutObj[field] = layout[field]; + const trimmedLayout = {}; + for (const field of fields) { + if (field in layout) { + trimmedLayout[field] = layout[field]; + } + } + return trimmedLayout; + } + + /** + * Method to identify the unique prefix (encountering first '/') of objects and add it as a set of labels to layout + * @param {object} layout - layout object to which labels will be added + * @returns {object} - layout object with added labels + */ + _addLabelsToLayout(layout) { + const labelsSet = new Set(); + layout.tabs?.forEach((tab) => { + tab.objects?.forEach((obj) => { + if (obj.name) { + const [prefix] = obj.name.split('/'); + labelsSet.add(prefix); + } }); - return layoutObj; }); + return { ...layout, labels: Array.from(labelsSet) }; } /** From 8ecb0097a44a08bf9e61672ef80d62fb1a50672c Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 12:56:22 +0100 Subject: [PATCH 02/10] Add tests to newly added layouts functionality --- .../test/api/layouts/api-get-layout.test.js | 4 +- .../lib/repositories/LayoutRepository.test.js | 450 +++++++++++++++++- 2 files changed, 436 insertions(+), 18 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..b27064b60 100644 --- a/QualityControl/test/api/layouts/api-get-layout.test.js +++ b/QualityControl/test/api/layouts/api-get-layout.test.js @@ -43,7 +43,9 @@ export const apiGetLayoutsTests = () => { if (!Array.isArray(res.body)) { throw new Error('Expected array of layouts'); } - + res.body.forEach((layout) => { + delete layout.labels; // remove labels for deep comparison + }); deepStrictEqual(res.body, [LAYOUT_MOCK_4, LAYOUT_MOCK_5], 'Unexpected Layout structure was returned'); }); }); diff --git a/QualityControl/test/lib/repositories/LayoutRepository.test.js b/QualityControl/test/lib/repositories/LayoutRepository.test.js index 84309ac09..d22105371 100644 --- a/QualityControl/test/lib/repositories/LayoutRepository.test.js +++ b/QualityControl/test/lib/repositories/LayoutRepository.test.js @@ -37,18 +37,18 @@ export const layoutRepositoryTest = async () => { jsonFileServiceMock.writeToFile.resetHistory(); }); - beforeEach(() => { - jsonFileServiceMock.writeToFile.resetHistory(); - }); - test('should initialize LayoutRepository successfully', () => { ok(layoutRepository); }); suite('list layouts', () => { - test('should list all layouts without filter', async () => { + test('should list all layouts without filter', () => { const result = layoutRepository.listLayouts(); strictEqual(result.length, 3, 'Length of list of layouts is not correct'); + result.forEach((layout) => { + ok(layout.labels, 'Each layout should have labels field'); + delete layout.labels; // remove labels for deep comparison + }); deepStrictEqual(result, jsonFileServiceMock.data.layouts, 'List of layouts filtered do not match the filters'); }); @@ -57,15 +57,15 @@ export const layoutRepositoryTest = async () => { const result = layoutRepository.listLayouts({ filter: { owner_id: ownerId } }); strictEqual(result.length, 2, 'number of layouts is incorrect'); - result.forEach((layout) => { + result.forEach((layout, index) => { + delete layout.labels; // remove labels for deep comparison strictEqual(layout.owner_id, ownerId, `Layout owner_id should be ${ownerId}`); + deepStrictEqual( + layout, + jsonFileServiceMock.data.layouts[index], + 'Filtered layout does not match expected layout', + ); }); - - deepStrictEqual( - result[0], - jsonFileServiceMock.data.layouts[0], - 'First layout should match the expected layout', - ); }); test('should return only layout with specified filter.objectPath', () => { @@ -73,7 +73,7 @@ export const layoutRepositoryTest = async () => { const result = layoutRepository.listLayouts({ filter: { objectPath, } }); - ok(result.length === 1, "listLayouts's filter.objectPath should only return one layout"); + strictEqual(result.length, 1, "listLayouts's filter.objectPath should only return one layout"); }); test('should return layouts with specified partial filter.objectPath', () => { @@ -81,7 +81,7 @@ export const layoutRepositoryTest = async () => { const result = layoutRepository.listLayouts({ filter: { objectPath, } }); - ok(result.length === 2, "listLayouts's filter.objectPath should only return 2 layouts"); + strictEqual(result.length, 2, "listLayouts's filter.objectPath should only return 2 layouts"); }); test('should return all layouts when filter.objectPath is empty string', () => { @@ -89,12 +89,12 @@ export const layoutRepositoryTest = async () => { const result = layoutRepository.listLayouts({ filter: { objectPath, } }); - ok(result.length === 3, "listLayouts's filter.objectPath should only return 3 (all) layouts"); + strictEqual(result.length, 3, "listLayouts's filter.objectPath should only return 3 (all) layouts"); }); test('should return all layouts when filter is an empty object', () => { const result = layoutRepository.listLayouts({ filter: {} }); - ok(result.length === 3, "listLayouts's empty filter object should only return 3 (all) layouts"); + strictEqual(result.length, 3, "listLayouts's empty filter object should only return 3 (all) layouts"); }); test('should return only specified fields when fields array is provided', () => { @@ -103,7 +103,22 @@ export const layoutRepositoryTest = async () => { result.forEach((layout) => { const actualKeys = Object.keys(layout); - deepStrictEqual(actualKeys.sort(), fields); + strictEqual(actualKeys.length, 2, 'Should have exactly 2 fields'); + deepStrictEqual(actualKeys.sort(), fields.sort()); + }); + + strictEqual(result.length, jsonFileServiceMock.data.layouts.length); + }); + + test('should include labels field when requested in fields array', () => { + const fields = ['id', 'name', 'labels']; + const result = layoutRepository.listLayouts({ fields }); + + result.forEach((layout) => { + const actualKeys = Object.keys(layout); + strictEqual(actualKeys.length, 3, 'Should have exactly 3 fields'); + ok(layout.labels, 'Should have labels field'); + ok(Array.isArray(layout.labels), 'Labels should be an array'); }); strictEqual(result.length, jsonFileServiceMock.data.layouts.length); @@ -116,6 +131,7 @@ export const layoutRepositoryTest = async () => { layoutRepository.readLayoutById('999'); }, NotFoundError); }); + test('should return a layout if it is found', () => { const layoutId = '671b95883d23cd0d67bdc787'; const layout = layoutRepository.readLayoutById(layoutId); @@ -210,5 +226,405 @@ export const layoutRepositoryTest = async () => { sinon.assert.calledOnce(jsonFileServiceMock.writeToFile); }); }); + + suite('\'_addLabelsToLayout\' test suite', () => { + test('should extract unique labels from multiple objects with different prefixes', () => { + const layout = { + id: '2', + name: 'Multi Prefix Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'qc_async/System/Monitor' }, + { id: 'obj3', name: 'qc_mc/Processing/Status' }, + ], + }, + ], + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc', 'qc_async', 'qc_mc'], 'Should contain all 3 labels'); + }); + + test('should handle duplicate prefixes and return unique labels only', () => { + const layout = { + id: '3', + name: 'Duplicate Prefix Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'qc/TPC/QO/Check2' }, + { id: 'obj3', name: 'qc/ITS/QO/Check3' }, + ], + }, + ], + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc'], 'Should extract "qc" as the only label'); + }); + + test('should handle multiple tabs with objects', () => { + const layout = { + id: '4', + name: 'Multi Tab Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + { + name: 'Tab2', + objects: [{ id: 'obj2', name: 'qc/System/Monitor' }], + }, + ], + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc'], 'Should include "qc" label from Tab2'); + }); + + test('should return empty labels array when layout has no tabs', () => { + const layout = { + id: '5', + name: 'No Tabs Layout', + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array'); + }); + + test('should return empty labels array when tabs have no objects', () => { + const layout = { + id: '6', + name: 'Empty Tabs Layout', + tabs: [{ name: 'Tab1' }], + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array'); + }); + + test('should return empty labels array when objects have no names', () => { + const layout = { + id: '7', + name: 'No Names Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1' }, { id: 'obj2' }], + }, + ], + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array when objects have no names'); + }); + + test('should handle objects with names that do not contain slashes', () => { + const layout = { + id: '8', + name: 'No Slash Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'simpleObject' }, + { id: 'obj2', name: 'anotherSimpleObject' }, + ], + }, + ], + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + strictEqual(result.labels.length, 2, 'Should extract full names as labels'); + ok(result.labels.includes('simpleObject'), 'Should include "simpleObject" as label'); + ok(result.labels.includes('anotherSimpleObject'), 'Should include "anotherSimpleObject" as label'); + }); + + test('should handle mixed objects with and without names', () => { + const layout = { + id: '9', + name: 'Mixed Objects Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2' }, + { id: 'obj3', name: 'qc/System/Monitor' }, + ], + }, + ], + }; + + const result = layoutRepository._addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc'], 'Should include "qc" label'); + }); + + test('should not mutate the original layout object', () => { + const layout = { + id: '10', + name: 'Original Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + + const originalTabsReference = layout.tabs; + const result = layoutRepository._addLabelsToLayout(layout); + + ok(!('labels' in layout), 'Original layout should not have labels property added'); + strictEqual(layout.tabs, originalTabsReference, 'Original layout tabs should not be mutated'); + ok(result.labels, 'Result should have labels property'); + }); + }); + + suite('\'_trimAndLabelLayout\' test suite', () => { + test('should return labeled layout with all fields when fields array is empty', () => { + const layout = { + id: '1', + name: 'Test Layout', + owner_id: 'user1', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'daq/System/Monitor' }, + ], + }, + ], + }; + + const result = layoutRepository._trimAndLabelLayout(layout, []); + + ok(result.labels, 'Should have labels field'); + strictEqual(result.id, layout.id, 'Should have id field'); + strictEqual(result.name, layout.name, 'Should have name field'); + strictEqual(result.owner_id, layout.owner_id, 'Should have owner_id field'); + deepStrictEqual(result.tabs, layout.tabs, 'Should have tabs field'); + strictEqual(result.labels.length, 2, 'Should have 2 labels'); + ok(result.labels.includes('qc'), 'Should include "qc" label'); + ok(result.labels.includes('daq'), 'Should include "daq" label'); + }); + + test('should return only specified fields when fields array is provided', () => { + const layout = { + id: '2', + name: 'Test Layout', + owner_id: 'user2', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['id', 'name']; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + const resultKeys = Object.keys(result); + strictEqual(resultKeys.length, 2, 'Should have exactly 2 fields'); + deepStrictEqual(resultKeys.sort(), fields.sort(), 'Should have only specified fields'); + strictEqual(result.id, layout.id, 'Should have correct id value'); + strictEqual(result.name, layout.name, 'Should have correct name value'); + ok(!result.owner_id, 'Should not have owner_id field'); + ok(!result.tabs, 'Should not have tabs field'); + ok(!result.labels, 'Should not have labels field when not requested'); + }); + + test('should include labels field when explicitly requested in fields array', () => { + const layout = { + id: '3', + name: 'Test Layout', + owner_id: 'user3', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'daq/System/Monitor' }, + ], + }, + ], + }; + const fields = ['id', 'name', 'labels']; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + const resultKeys = Object.keys(result); + strictEqual(resultKeys.length, 3, 'Should have exactly 3 fields'); + ok(result.labels, 'Should have labels field'); + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 2, 'Should have 2 labels'); + ok(result.labels.includes('qc'), 'Should include "qc" label'); + ok(result.labels.includes('daq'), 'Should include "daq" label'); + strictEqual(result.id, layout.id, 'Should have correct id value'); + strictEqual(result.name, layout.name, 'Should have correct name value'); + }); + + test('should handle fields array with non-existent field names', () => { + const layout = { + id: '4', + name: 'Test Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['id', 'name', 'nonExistentField', 'anotherNonExistent']; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + const resultKeys = Object.keys(result); + strictEqual(resultKeys.length, 2, 'Should only include existing fields'); + ok(result.id, 'Should have id field'); + ok(result.name, 'Should have name field'); + ok(!result.nonExistentField, 'Should not have nonExistentField'); + ok(!result.anotherNonExistent, 'Should not have anotherNonExistent'); + }); + + test('should handle layout with no tabs', () => { + const layout = { + id: '5', + name: 'No Tabs Layout', + owner_id: 'user5', + }; + const fields = ['id', 'name', 'labels']; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + strictEqual(result.id, layout.id, 'Should have id field'); + strictEqual(result.name, layout.name, 'Should have name field'); + ok(Array.isArray(result.labels), 'Should have labels field as array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array'); + }); + + test('should handle layout with tabs but no objects', () => { + const layout = { + id: '6', + name: 'Empty Tabs Layout', + owner_id: 'user6', + tabs: [{ name: 'Tab1' }], + }; + const fields = ['id', 'labels']; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + strictEqual(result.id, layout.id, 'Should have id field'); + ok(Array.isArray(result.labels), 'Should have labels field as array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array'); + ok(!result.name, 'Should not have name field'); + ok(!result.tabs, 'Should not have tabs field'); + }); + + test('should not mutate the original layout object', () => { + const layout = { + id: '7', + name: 'Original Layout', + owner_id: 'user7', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['id', 'name', 'labels']; + const originalTabsReference = layout.tabs; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + ok(!('labels' in layout), 'Original layout should not have labels property added'); + strictEqual(layout.tabs, originalTabsReference, 'Original layout tabs should not be mutated'); + strictEqual(layout.owner_id, 'user7', 'Original layout owner_id should be unchanged'); + ok(result.labels, 'Result should have labels property'); + strictEqual(Object.keys(result).length, 3, 'Result should have only requested fields'); + }); + + test('should correctly trim complex layout with multiple tabs and objects', () => { + const layout = { + id: '8', + name: 'Complex Layout', + owner_id: 'user8', + createdAt: '2026-01-17', + updatedAt: '2026-01-17', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'qc/TPC/QO/Check2' }, + ], + }, + { + name: 'Tab2', + objects: [{ id: 'obj3', name: 'daq/System/Monitor' }], + }, + ], + }; + const fields = ['id', 'name', 'labels', 'owner_id']; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + strictEqual(Object.keys(result).length, 4, 'Should have exactly 4 fields'); + strictEqual(result.id, layout.id); + strictEqual(result.name, layout.name); + strictEqual(result.owner_id, layout.owner_id); + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 2, 'Should have 2 unique labels'); + ok(result.labels.includes('qc'), 'Should include "qc" label'); + ok(result.labels.includes('daq'), 'Should include "daq" label'); + ok(!result.tabs, 'Should not include tabs field'); + ok(!result.createdAt, 'Should not include createdAt field'); + ok(!result.updatedAt, 'Should not include updatedAt field'); + }); + + test('should handle single field selection', () => { + const layout = { + id: '9', + name: 'Single Field Layout', + owner_id: 'user9', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['name']; + + const result = layoutRepository._trimAndLabelLayout(layout, fields); + + strictEqual(Object.keys(result).length, 1, 'Should have exactly 1 field'); + strictEqual(result.name, layout.name, 'Should have correct name value'); + ok(!result.id, 'Should not have id field'); + ok(!result.owner_id, 'Should not have owner_id field'); + ok(!result.labels, 'Should not have labels field'); + ok(!result.tabs, 'Should not have tabs field'); + }); + }); }); }; From 509740a8195715c38160985d06b1bb4723c3fb9e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 12:56:28 +0100 Subject: [PATCH 03/10] Add tests to be ran --- QualityControl/test/test-index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/QualityControl/test/test-index.js b/QualityControl/test/test-index.js index 87c60915d..cffd93b55 100644 --- a/QualityControl/test/test-index.js +++ b/QualityControl/test/test-index.js @@ -65,6 +65,7 @@ import { bookkeepingServiceTestSuite } from './lib/services/BookkeepingService.t */ import { baseRepositoryTestSuite } from './lib/database/repositories/BaseRepository.test.js'; import { layoutRepositoryTestSuite } from './lib/database/repositories/LayoutRepository.test.js'; +import { layoutRepositoryTest } from './lib/repositories/LayoutRepository.test.js'; import { userRepositoryTestSuite } from './lib/database/repositories/UserRepository.test.js'; import { chartRepositoryTestSuite } from './lib/database/repositories/ChartRepository.test.js'; import { chartOptionsRepositoryTestSuite } from './lib/database/repositories/ChartOptionsRepository.test.js'; @@ -241,7 +242,8 @@ suite('All Tests - QCG', { timeout: FRONT_END_TIMEOUT + BACK_END_TIMEOUT }, asyn suite('Repositories - Test Suite', async () => { suite('Base Repository - Test Suite', async () => await baseRepositoryTestSuite()); - suite('Layout Repository - Test Suite', async () => await layoutRepositoryTestSuite()); + 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()); From 671465c99959e95cb955a229abdb388a8489159e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 13:43:50 +0100 Subject: [PATCH 04/10] Extract components of layout as not dependent on class --- .../lib/utils/layout/addLabelsToLayout.js | 30 +++ .../layout/trimLayoutPerRequiredFields.js | 31 +++ .../utils/layout/addLabelsToLayout.test.js | 189 +++++++++++++++ .../trimLayoutPerRequiredFields.test.js | 227 ++++++++++++++++++ 4 files changed, 477 insertions(+) create mode 100644 QualityControl/lib/utils/layout/addLabelsToLayout.js create mode 100644 QualityControl/lib/utils/layout/trimLayoutPerRequiredFields.js create mode 100644 QualityControl/test/lib/utils/layout/addLabelsToLayout.test.js create mode 100644 QualityControl/test/lib/utils/layout/trimLayoutPerRequiredFields.test.js diff --git a/QualityControl/lib/utils/layout/addLabelsToLayout.js b/QualityControl/lib/utils/layout/addLabelsToLayout.js new file mode 100644 index 000000000..5541f3730 --- /dev/null +++ b/QualityControl/lib/utils/layout/addLabelsToLayout.js @@ -0,0 +1,30 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Method to identify the unique prefix (encountering first '/') of objects and add it as a set of labels to layout + * @param {LayoutDto} layout - layout object to which labels will be added + * @returns {{...LayoutDto, labels: string[]}} - layout object with added labels + */ +export const addLabelsToLayout = (layout) => { + const labelsSet = new Set(); + layout.tabs?.forEach((tab) => { + tab.objects?.forEach((obj) => { + if (obj.name) { + const [prefix] = obj.name.split('/'); + labelsSet.add(prefix); + } + }); + }); + return { ...layout, labels: Array.from(labelsSet) }; +}; diff --git a/QualityControl/lib/utils/layout/trimLayoutPerRequiredFields.js b/QualityControl/lib/utils/layout/trimLayoutPerRequiredFields.js new file mode 100644 index 000000000..085f099e7 --- /dev/null +++ b/QualityControl/lib/utils/layout/trimLayoutPerRequiredFields.js @@ -0,0 +1,31 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Trims a layout object to only include requested fields + * @param {LayoutDto} layout - layout object to be trimmed + * @param {string[]} [fields = []] - Array of field names to include in the returned layout object + * @returns {Partial} - Trimmed layout object + */ +export const trimLayoutPerRequiredFields = (layout, fields = []) => { + if (fields.length === 0) { + return layout; + } + const trimmedLayout = {}; + for (const field of fields) { + if (field in layout) { + trimmedLayout[field] = layout[field]; + } + } + return trimmedLayout; +}; diff --git a/QualityControl/test/lib/utils/layout/addLabelsToLayout.test.js b/QualityControl/test/lib/utils/layout/addLabelsToLayout.test.js new file mode 100644 index 000000000..10d344d3b --- /dev/null +++ b/QualityControl/test/lib/utils/layout/addLabelsToLayout.test.js @@ -0,0 +1,189 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { suite, test } from 'node:test'; +import { deepStrictEqual, ok, strictEqual } from 'node:assert'; +import { addLabelsToLayout } from '../../../../lib/utils/layout/addLabelsToLayout.js'; + +export const addLabelsToLayoutTestSuite = () => { + suite('\'addLabelsToLayout\' test suite', () => { + test('should extract unique labels from multiple objects with different prefixes', () => { + const layout = { + id: '2', + name: 'Multi Prefix Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'qc_async/System/Monitor' }, + { id: 'obj3', name: 'qc_mc/Processing/Status' }, + ], + }, + ], + }; + + const result = addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc', 'qc_async', 'qc_mc'], 'Should contain all 3 labels'); + }); + + test('should handle duplicate prefixes and return unique labels only', () => { + const layout = { + id: '3', + name: 'Duplicate Prefix Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'qc/TPC/QO/Check2' }, + { id: 'obj3', name: 'qc/ITS/QO/Check3' }, + ], + }, + ], + }; + + const result = addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc'], 'Should extract "qc" as the only label'); + }); + + test('should handle multiple tabs with objects', () => { + const layout = { + id: '4', + name: 'Multi Tab Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + { + name: 'Tab2', + objects: [{ id: 'obj2', name: 'qc/System/Monitor' }], + }, + ], + }; + + const result = addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc'], 'Should include "qc" label from Tab2'); + }); + + test('should return empty labels array when layout has no tabs', () => { + const layout = { + id: '5', + name: 'No Tabs Layout', + }; + + const result = addLabelsToLayout(layout); + + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array'); + }); + + test('should return empty labels array when tabs have no objects', () => { + const layout = { + id: '6', + name: 'Empty Tabs Layout', + tabs: [{ name: 'Tab1' }], + }; + + const result = addLabelsToLayout(layout); + + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array'); + }); + + test('should return empty labels array when objects have no names', () => { + const layout = { + id: '7', + name: 'No Names Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1' }, { id: 'obj2' }], + }, + ], + }; + + const result = addLabelsToLayout(layout); + + ok(Array.isArray(result.labels), 'Labels should be an array'); + strictEqual(result.labels.length, 0, 'Should have empty labels array when objects have no names'); + }); + + test('should handle objects with names that do not contain slashes', () => { + const layout = { + id: '8', + name: 'No Slash Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'simpleObject' }, + { id: 'obj2', name: 'anotherSimpleObject' }, + ], + }, + ], + }; + + const result = addLabelsToLayout(layout); + + strictEqual(result.labels.length, 2, 'Should extract full names as labels'); + ok(result.labels.includes('simpleObject'), 'Should include "simpleObject" as label'); + ok(result.labels.includes('anotherSimpleObject'), 'Should include "anotherSimpleObject" as label'); + }); + + test('should handle mixed objects with and without names', () => { + const layout = { + id: '9', + name: 'Mixed Objects Layout', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2' }, + { id: 'obj3', name: 'qc/System/Monitor' }, + ], + }, + ], + }; + + const result = addLabelsToLayout(layout); + + deepStrictEqual(result.labels, ['qc'], 'Should include "qc" label'); + }); + + test('should not mutate the original layout object', () => { + const layout = { + id: '10', + name: 'Original Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + + const originalTabsReference = layout.tabs; + const result = addLabelsToLayout(layout); + + ok(!('labels' in layout), 'Original layout should not have labels property added'); + strictEqual(layout.tabs, originalTabsReference, 'Original layout tabs should not be mutated'); + ok(result.labels, 'Result should have labels property'); + }); + }); +}; diff --git a/QualityControl/test/lib/utils/layout/trimLayoutPerRequiredFields.test.js b/QualityControl/test/lib/utils/layout/trimLayoutPerRequiredFields.test.js new file mode 100644 index 000000000..457720e3c --- /dev/null +++ b/QualityControl/test/lib/utils/layout/trimLayoutPerRequiredFields.test.js @@ -0,0 +1,227 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { suite, test } from 'node:test'; +import { deepStrictEqual, ok, strictEqual } from 'node:assert'; +import { trimLayoutPerRequiredFields } from '../../../../lib/utils/layout/trimLayoutPerRequiredFields.js'; + +export const trimLayoutPerRequiredFieldsTestSuite = () => { + suite('\'trimLayoutPerRequiredFields\' test suite', () => { + test('should return labeled layout with all fields when fields array is empty', () => { + const layout = { + id: '1', + name: 'Test Layout', + owner_id: 'user1', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'daq/System/Monitor' }, + ], + }, + ], + }; + + const result = trimLayoutPerRequiredFields(layout, []); + + strictEqual(result.id, layout.id, 'Should have id field'); + strictEqual(result.name, layout.name, 'Should have name field'); + strictEqual(result.owner_id, layout.owner_id, 'Should have owner_id field'); + deepStrictEqual(result.tabs, layout.tabs, 'Should have tabs field'); + }); + + test('should return only specified fields when fields array is provided', () => { + const layout = { + id: '2', + name: 'Test Layout', + owner_id: 'user2', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['id', 'name']; + + const result = trimLayoutPerRequiredFields(layout, fields); + + const resultKeys = Object.keys(result); + strictEqual(resultKeys.length, 2, 'Should have exactly 2 fields'); + deepStrictEqual(resultKeys.sort(), fields.sort(), 'Should have only specified fields'); + strictEqual(result.id, layout.id, 'Should have correct id value'); + strictEqual(result.name, layout.name, 'Should have correct name value'); + ok(!result.owner_id, 'Should not have owner_id field'); + ok(!result.tabs, 'Should not have tabs field'); + }); + + test('should not include labels field when explicitly requested if not in layout', () => { + const layout = { + id: '3', + name: 'Test Layout', + owner_id: 'user3', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'daq/System/Monitor' }, + ], + }, + ], + }; + const fields = ['id', 'name', 'labels']; + const result = trimLayoutPerRequiredFields(layout, fields); + + const resultKeys = Object.keys(result); + strictEqual(resultKeys.length, 2, 'Should have exactly 3 fields'); + ok(!result.labels, 'Should have labels field'); + strictEqual(result.id, layout.id, 'Should have correct id value'); + strictEqual(result.name, layout.name, 'Should have correct name value'); + }); + + test('should handle fields array with non-existent field names', () => { + const layout = { + id: '4', + name: 'Test Layout', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['id', 'name', 'nonExistentField', 'anotherNonExistent']; + + const result = trimLayoutPerRequiredFields(layout, fields); + + const resultKeys = Object.keys(result); + strictEqual(resultKeys.length, 2, 'Should only include existing fields'); + ok(result.id, 'Should have id field'); + ok(result.name, 'Should have name field'); + ok(!result.nonExistentField, 'Should not have nonExistentField'); + ok(!result.anotherNonExistent, 'Should not have anotherNonExistent'); + }); + + test('should handle layout with no tabs', () => { + const layout = { + id: '5', + name: 'No Tabs Layout', + owner_id: 'user5', + }; + const fields = ['id', 'name']; + + const result = trimLayoutPerRequiredFields(layout, fields); + + strictEqual(result.id, layout.id, 'Should have id field'); + strictEqual(result.name, layout.name, 'Should have name field'); + }); + + test('should handle layout with tabs but no objects', () => { + const layout = { + id: '6', + name: 'Empty Tabs Layout', + owner_id: 'user6', + tabs: [{ name: 'Tab1' }], + }; + const fields = ['id']; + + const result = trimLayoutPerRequiredFields(layout, fields); + + strictEqual(result.id, layout.id, 'Should have id field'); + ok(!result.name, 'Should not have name field'); + ok(!result.tabs, 'Should not have tabs field'); + }); + + test('should not mutate the original layout object', () => { + const layout = { + id: '7', + name: 'Original Layout', + owner_id: 'user7', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['id', 'name']; + const originalTabsReference = layout.tabs; + + const result = trimLayoutPerRequiredFields(layout, fields); + + strictEqual(layout.tabs, originalTabsReference, 'Original layout tabs should not be mutated'); + strictEqual(layout.owner_id, 'user7', 'Original layout owner_id should be unchanged'); + strictEqual(Object.keys(result).length, 2, 'Result should have only requested fields'); + }); + + test('should correctly trim complex layout with multiple tabs and objects', () => { + const layout = { + id: '8', + name: 'Complex Layout', + owner_id: 'user8', + createdAt: '2026-01-17', + updatedAt: '2026-01-17', + tabs: [ + { + name: 'Tab1', + objects: [ + { id: 'obj1', name: 'qc/MCH/QO/Check1' }, + { id: 'obj2', name: 'qc/TPC/QO/Check2' }, + ], + }, + { + name: 'Tab2', + objects: [{ id: 'obj3', name: 'daq/System/Monitor' }], + }, + ], + }; + const fields = ['id', 'name', 'owner_id']; + + const result = trimLayoutPerRequiredFields(layout, fields); + + strictEqual(Object.keys(result).length, 3, 'Should have exactly 3 fields'); + strictEqual(result.id, layout.id); + strictEqual(result.name, layout.name); + strictEqual(result.owner_id, layout.owner_id); + ok(!result.tabs, 'Should not include tabs field'); + ok(!result.createdAt, 'Should not include createdAt field'); + ok(!result.updatedAt, 'Should not include updatedAt field'); + }); + + test('should handle single field selection', () => { + const layout = { + id: '9', + name: 'Single Field Layout', + owner_id: 'user9', + tabs: [ + { + name: 'Tab1', + objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], + }, + ], + }; + const fields = ['name']; + + const result = trimLayoutPerRequiredFields(layout, fields); + + strictEqual(Object.keys(result).length, 1, 'Should have exactly 1 field'); + strictEqual(result.name, layout.name, 'Should have correct name value'); + ok(!result.id, 'Should not have id field'); + ok(!result.owner_id, 'Should not have owner_id field'); + ok(!result.labels, 'Should not have labels field'); + ok(!result.tabs, 'Should not have tabs field'); + }); + }); +}; From e3014f308adb0e8a8bfe36c67509a24fec61af4f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 13:45:08 +0100 Subject: [PATCH 05/10] Use extracted components and allow labels fetch --- QualityControl/lib/dtos/LayoutDto.js | 1 + .../lib/repositories/LayoutRepository.js | 71 +-- .../test/api/layouts/api-get-layout.test.js | 11 +- .../lib/repositories/LayoutRepository.test.js | 418 +----------------- QualityControl/test/test-index.js | 4 + 5 files changed, 35 insertions(+), 470 deletions(-) diff --git a/QualityControl/lib/dtos/LayoutDto.js b/QualityControl/lib/dtos/LayoutDto.js index a285ddb94..2b6b383ea 100644 --- a/QualityControl/lib/dtos/LayoutDto.js +++ b/QualityControl/lib/dtos/LayoutDto.js @@ -26,6 +26,7 @@ const ALLOWED_LAYOUT_FIELDS = [ 'owner_id', 'owner_name', 'tabs', + 'labels', ]; /** diff --git a/QualityControl/lib/repositories/LayoutRepository.js b/QualityControl/lib/repositories/LayoutRepository.js index 3c342f476..efe966d1e 100644 --- a/QualityControl/lib/repositories/LayoutRepository.js +++ b/QualityControl/lib/repositories/LayoutRepository.js @@ -13,6 +13,8 @@ import { NotFoundError } from '@aliceo2/web-ui'; import { BaseRepository } from './BaseRepository.js'; +import { addLabelsToLayout } from '../utils/layout/addLabelsToLayout.js'; +import { trimLayoutPerRequiredFields } from '../utils/layout/trimLayoutPerRequiredFields.js'; /** * LayoutRepository class to handle CRUD operations for Layouts. @@ -31,58 +33,13 @@ export class LayoutRepository extends BaseRepository { const filteredLayouts = this._filterLayouts(layouts, { ...filter, name }); const trimmedAndLabelledLayouts = filteredLayouts - .map((layout) => this._trimAndLabelLayout(layout, fields)); - return trimmedAndLabelledLayouts; - } - - /** - * Adds labels field to a layout based on contained objects in the layout tabs. - * Then, trims a layout object to only include requested fields and adds labels - * @param {object} layout - layout object to be processed - * @param {Array} fields - Array of field names to include in the returned layout object - * @returns {object} - Processed layout object with added labels and trimmed fields - */ - _trimAndLabelLayout(layout, fields) { - const labeledLayout = this._addLabelsToLayout(layout); - const trimmedLayout = this._trimLayout(labeledLayout, fields); - return trimmedLayout; - } - - /** - * Trims a layout object to only include requested fields - * @param {object} layout - layout object to be trimmed - * @param {Array} [fields = []] - Array of field names to include in the returned layout object - * @returns {object} - Trimmed layout object - */ - _trimLayout(layout, fields = []) { - if (fields.length === 0) { - return layout; - } - const trimmedLayout = {}; - for (const field of fields) { - if (field in layout) { - trimmedLayout[field] = layout[field]; - } - } - return trimmedLayout; - } - - /** - * Method to identify the unique prefix (encountering first '/') of objects and add it as a set of labels to layout - * @param {object} layout - layout object to which labels will be added - * @returns {object} - layout object with added labels - */ - _addLabelsToLayout(layout) { - const labelsSet = new Set(); - layout.tabs?.forEach((tab) => { - tab.objects?.forEach((obj) => { - if (obj.name) { - const [prefix] = obj.name.split('/'); - labelsSet.add(prefix); - } + .map((layout) => { + const labeledLayout = addLabelsToLayout(layout); + const trimmedLayout = trimLayoutPerRequiredFields(labeledLayout, fields); + return trimmedLayout; }); - }); - return { ...layout, labels: Array.from(labelsSet) }; + + return trimmedAndLabelledLayouts; } /** @@ -122,25 +79,27 @@ export class LayoutRepository extends BaseRepository { * @throws {NotFoundError} - if the layout is not found */ readLayoutById(layoutId) { - const foundLayout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId); - if (!foundLayout) { + const layout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId); + if (!layout) { throw new NotFoundError(`layout (${layoutId}) not found`); } - return foundLayout; + const labeledLayout = addLabelsToLayout(layout); + return labeledLayout; } /** * Given a string, representing layout name, retrieve the layout if it exists * @param {string} layoutName - name of the layout to retrieve * @returns {Layout} - object with layout information - * @throws + * @throws {NotFoundError} - if the layout is not found */ readLayoutByName(layoutName) { const layout = this._jsonFileService.data.layouts.find((layout) => layout.name === layoutName); if (!layout) { throw new NotFoundError(`Layout (${layoutName}) not found`); } - return layout; + const labeledLayout = addLabelsToLayout(layout); + return labeledLayout; } /** diff --git a/QualityControl/test/api/layouts/api-get-layout.test.js b/QualityControl/test/api/layouts/api-get-layout.test.js index b27064b60..2a5c99f07 100644 --- a/QualityControl/test/api/layouts/api-get-layout.test.js +++ b/QualityControl/test/api/layouts/api-get-layout.test.js @@ -17,6 +17,7 @@ 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 { addLabelsToLayout } from '../../../lib/utils/layout/addLabelsToLayout.js'; export const apiGetLayoutsTests = () => { suite('GET /layouts', () => { @@ -82,10 +83,11 @@ export const apiGetLayoutsTests = () => { suite('GET /layout/:id', () => { test('should return a single layout by id', async () => { const layoutId = '671b8c22402408122e2f20dd'; + const expectedLayout = addLabelsToLayout(LAYOUT_MOCK_6); await request(`${URL_ADDRESS}/api/layout/${layoutId}`) .get(`?token=${OWNER_TEST_TOKEN}`) .expect(200) - .expect((res) => deepStrictEqual(res.body, LAYOUT_MOCK_6, 'Unexpected Layout structure was returned')); + .expect((res) => deepStrictEqual(res.body, expectedLayout, 'Unexpected Layout structure was returned')); }); test('should return 400 when id parameter is an empty string', async () => { @@ -105,19 +107,22 @@ export const apiGetLayoutsTests = () => { suite('GET /layout?name=', () => { test('should return layout by name', async () => { const layoutName = 'a-test'; + const expectedLayout = addLabelsToLayout(LAYOUT_MOCK_5); await request(`${URL_ADDRESS}/api/layout`) .get(`?token=${OWNER_TEST_TOKEN}&name=${layoutName}`) .expect(200) - .expect((res) => deepStrictEqual(res.body, LAYOUT_MOCK_5, 'Unexpected Layout structure was returned')); + .expect((res) => deepStrictEqual(res.body, expectedLayout, 'Unexpected Layout structure was returned')); }); test('should return layout by runDefinition', async () => { const runDefinition = 'a-test'; + const expectedLayout = addLabelsToLayout(LAYOUT_MOCK_5); await request(`${URL_ADDRESS}/api/layout`) .get(`?token=${OWNER_TEST_TOKEN}&runDefinition=${runDefinition}`) .expect(200) - .expect((res) => deepStrictEqual(res.body, LAYOUT_MOCK_5, 'Unexpected Layout structure was returned')); + .expect((res) => deepStrictEqual(res.body, expectedLayout, 'Unexpected Layout structure was returned')); }); + test('should return layout by runDefinition and pdpBeamType combination', async () => { const runDefinition = 'rundefinition'; const pdpBeamType = 'pdpBeamType'; diff --git a/QualityControl/test/lib/repositories/LayoutRepository.test.js b/QualityControl/test/lib/repositories/LayoutRepository.test.js index d22105371..383ab262f 100644 --- a/QualityControl/test/lib/repositories/LayoutRepository.test.js +++ b/QualityControl/test/lib/repositories/LayoutRepository.test.js @@ -12,11 +12,12 @@ */ import { suite, test, before, beforeEach } from 'node:test'; -import { LayoutRepository } from '../../../lib/repositories/LayoutRepository.js'; import { deepEqual, deepStrictEqual, ok, rejects, strictEqual, throws } from 'node:assert'; import { NotFoundError } from '@aliceo2/web-ui'; import sinon from 'sinon'; import { initTest } from '../../setup/testRepositorySetup.js'; +import { LayoutRepository } from '../../../lib/repositories/LayoutRepository.js'; +import { addLabelsToLayout } from '../../../lib/utils/layout/addLabelsToLayout.js'; /** * @typedef {import('../../../lib/services/JsonFileService.js').JsonFileService} JsonFileService @@ -135,7 +136,7 @@ export const layoutRepositoryTest = async () => { test('should return a layout if it is found', () => { const layoutId = '671b95883d23cd0d67bdc787'; const layout = layoutRepository.readLayoutById(layoutId); - const expectedLayout = jsonFileServiceMock.data.layouts.find((l) => l.id === layoutId); + const expectedLayout = addLabelsToLayout(jsonFileServiceMock.data.layouts.find((l) => l.id === layoutId)); strictEqual(layout.id, layoutId); deepStrictEqual(layout, expectedLayout); strictEqual(layout.name, expectedLayout.name); @@ -185,20 +186,15 @@ export const layoutRepositoryTest = async () => { suite('update layouts', () => { test('should update a single layout by its id', async () => { + const idOfLayoutToUpdate = '671b8c22402408122e2f20dd'; const newLayout = { - id: '671b8c22402408122e2f20dd', + id: idOfLayoutToUpdate, name: 'Test Layout Updated', owner_id: 'user1', tabs: [{ name: 'Tab1', objects: [{ id: '1', name: 'Object1' }] }], }; - const idOfLayoutUpdated = await layoutRepository.updateLayout('671b8c22402408122e2f20dd', newLayout); - strictEqual(idOfLayoutUpdated, '671b8c22402408122e2f20dd'); - - const updatedLayout = jsonFileServiceMock.data.layouts.find((l) => l.id === newLayout.id); - strictEqual(updatedLayout.id, newLayout.id); - strictEqual(updatedLayout.name, newLayout.name); - strictEqual(updatedLayout.owner_id, newLayout.owner_id); - deepStrictEqual(updatedLayout.tabs, newLayout.tabs); + const idOfLayoutUpdated = await layoutRepository.updateLayout(idOfLayoutToUpdate, newLayout); + strictEqual(idOfLayoutUpdated, idOfLayoutToUpdate); sinon.assert.calledOnce(jsonFileServiceMock.writeToFile); }); @@ -226,405 +222,5 @@ export const layoutRepositoryTest = async () => { sinon.assert.calledOnce(jsonFileServiceMock.writeToFile); }); }); - - suite('\'_addLabelsToLayout\' test suite', () => { - test('should extract unique labels from multiple objects with different prefixes', () => { - const layout = { - id: '2', - name: 'Multi Prefix Layout', - tabs: [ - { - name: 'Tab1', - objects: [ - { id: 'obj1', name: 'qc/MCH/QO/Check1' }, - { id: 'obj2', name: 'qc_async/System/Monitor' }, - { id: 'obj3', name: 'qc_mc/Processing/Status' }, - ], - }, - ], - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - deepStrictEqual(result.labels, ['qc', 'qc_async', 'qc_mc'], 'Should contain all 3 labels'); - }); - - test('should handle duplicate prefixes and return unique labels only', () => { - const layout = { - id: '3', - name: 'Duplicate Prefix Layout', - tabs: [ - { - name: 'Tab1', - objects: [ - { id: 'obj1', name: 'qc/MCH/QO/Check1' }, - { id: 'obj2', name: 'qc/TPC/QO/Check2' }, - { id: 'obj3', name: 'qc/ITS/QO/Check3' }, - ], - }, - ], - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - deepStrictEqual(result.labels, ['qc'], 'Should extract "qc" as the only label'); - }); - - test('should handle multiple tabs with objects', () => { - const layout = { - id: '4', - name: 'Multi Tab Layout', - tabs: [ - { - name: 'Tab1', - objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], - }, - { - name: 'Tab2', - objects: [{ id: 'obj2', name: 'qc/System/Monitor' }], - }, - ], - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - deepStrictEqual(result.labels, ['qc'], 'Should include "qc" label from Tab2'); - }); - - test('should return empty labels array when layout has no tabs', () => { - const layout = { - id: '5', - name: 'No Tabs Layout', - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - ok(Array.isArray(result.labels), 'Labels should be an array'); - strictEqual(result.labels.length, 0, 'Should have empty labels array'); - }); - - test('should return empty labels array when tabs have no objects', () => { - const layout = { - id: '6', - name: 'Empty Tabs Layout', - tabs: [{ name: 'Tab1' }], - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - ok(Array.isArray(result.labels), 'Labels should be an array'); - strictEqual(result.labels.length, 0, 'Should have empty labels array'); - }); - - test('should return empty labels array when objects have no names', () => { - const layout = { - id: '7', - name: 'No Names Layout', - tabs: [ - { - name: 'Tab1', - objects: [{ id: 'obj1' }, { id: 'obj2' }], - }, - ], - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - ok(Array.isArray(result.labels), 'Labels should be an array'); - strictEqual(result.labels.length, 0, 'Should have empty labels array when objects have no names'); - }); - - test('should handle objects with names that do not contain slashes', () => { - const layout = { - id: '8', - name: 'No Slash Layout', - tabs: [ - { - name: 'Tab1', - objects: [ - { id: 'obj1', name: 'simpleObject' }, - { id: 'obj2', name: 'anotherSimpleObject' }, - ], - }, - ], - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - strictEqual(result.labels.length, 2, 'Should extract full names as labels'); - ok(result.labels.includes('simpleObject'), 'Should include "simpleObject" as label'); - ok(result.labels.includes('anotherSimpleObject'), 'Should include "anotherSimpleObject" as label'); - }); - - test('should handle mixed objects with and without names', () => { - const layout = { - id: '9', - name: 'Mixed Objects Layout', - tabs: [ - { - name: 'Tab1', - objects: [ - { id: 'obj1', name: 'qc/MCH/QO/Check1' }, - { id: 'obj2' }, - { id: 'obj3', name: 'qc/System/Monitor' }, - ], - }, - ], - }; - - const result = layoutRepository._addLabelsToLayout(layout); - - deepStrictEqual(result.labels, ['qc'], 'Should include "qc" label'); - }); - - test('should not mutate the original layout object', () => { - const layout = { - id: '10', - name: 'Original Layout', - tabs: [ - { - name: 'Tab1', - objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], - }, - ], - }; - - const originalTabsReference = layout.tabs; - const result = layoutRepository._addLabelsToLayout(layout); - - ok(!('labels' in layout), 'Original layout should not have labels property added'); - strictEqual(layout.tabs, originalTabsReference, 'Original layout tabs should not be mutated'); - ok(result.labels, 'Result should have labels property'); - }); - }); - - suite('\'_trimAndLabelLayout\' test suite', () => { - test('should return labeled layout with all fields when fields array is empty', () => { - const layout = { - id: '1', - name: 'Test Layout', - owner_id: 'user1', - tabs: [ - { - name: 'Tab1', - objects: [ - { id: 'obj1', name: 'qc/MCH/QO/Check1' }, - { id: 'obj2', name: 'daq/System/Monitor' }, - ], - }, - ], - }; - - const result = layoutRepository._trimAndLabelLayout(layout, []); - - ok(result.labels, 'Should have labels field'); - strictEqual(result.id, layout.id, 'Should have id field'); - strictEqual(result.name, layout.name, 'Should have name field'); - strictEqual(result.owner_id, layout.owner_id, 'Should have owner_id field'); - deepStrictEqual(result.tabs, layout.tabs, 'Should have tabs field'); - strictEqual(result.labels.length, 2, 'Should have 2 labels'); - ok(result.labels.includes('qc'), 'Should include "qc" label'); - ok(result.labels.includes('daq'), 'Should include "daq" label'); - }); - - test('should return only specified fields when fields array is provided', () => { - const layout = { - id: '2', - name: 'Test Layout', - owner_id: 'user2', - tabs: [ - { - name: 'Tab1', - objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], - }, - ], - }; - const fields = ['id', 'name']; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - const resultKeys = Object.keys(result); - strictEqual(resultKeys.length, 2, 'Should have exactly 2 fields'); - deepStrictEqual(resultKeys.sort(), fields.sort(), 'Should have only specified fields'); - strictEqual(result.id, layout.id, 'Should have correct id value'); - strictEqual(result.name, layout.name, 'Should have correct name value'); - ok(!result.owner_id, 'Should not have owner_id field'); - ok(!result.tabs, 'Should not have tabs field'); - ok(!result.labels, 'Should not have labels field when not requested'); - }); - - test('should include labels field when explicitly requested in fields array', () => { - const layout = { - id: '3', - name: 'Test Layout', - owner_id: 'user3', - tabs: [ - { - name: 'Tab1', - objects: [ - { id: 'obj1', name: 'qc/MCH/QO/Check1' }, - { id: 'obj2', name: 'daq/System/Monitor' }, - ], - }, - ], - }; - const fields = ['id', 'name', 'labels']; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - const resultKeys = Object.keys(result); - strictEqual(resultKeys.length, 3, 'Should have exactly 3 fields'); - ok(result.labels, 'Should have labels field'); - ok(Array.isArray(result.labels), 'Labels should be an array'); - strictEqual(result.labels.length, 2, 'Should have 2 labels'); - ok(result.labels.includes('qc'), 'Should include "qc" label'); - ok(result.labels.includes('daq'), 'Should include "daq" label'); - strictEqual(result.id, layout.id, 'Should have correct id value'); - strictEqual(result.name, layout.name, 'Should have correct name value'); - }); - - test('should handle fields array with non-existent field names', () => { - const layout = { - id: '4', - name: 'Test Layout', - tabs: [ - { - name: 'Tab1', - objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], - }, - ], - }; - const fields = ['id', 'name', 'nonExistentField', 'anotherNonExistent']; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - const resultKeys = Object.keys(result); - strictEqual(resultKeys.length, 2, 'Should only include existing fields'); - ok(result.id, 'Should have id field'); - ok(result.name, 'Should have name field'); - ok(!result.nonExistentField, 'Should not have nonExistentField'); - ok(!result.anotherNonExistent, 'Should not have anotherNonExistent'); - }); - - test('should handle layout with no tabs', () => { - const layout = { - id: '5', - name: 'No Tabs Layout', - owner_id: 'user5', - }; - const fields = ['id', 'name', 'labels']; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - strictEqual(result.id, layout.id, 'Should have id field'); - strictEqual(result.name, layout.name, 'Should have name field'); - ok(Array.isArray(result.labels), 'Should have labels field as array'); - strictEqual(result.labels.length, 0, 'Should have empty labels array'); - }); - - test('should handle layout with tabs but no objects', () => { - const layout = { - id: '6', - name: 'Empty Tabs Layout', - owner_id: 'user6', - tabs: [{ name: 'Tab1' }], - }; - const fields = ['id', 'labels']; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - strictEqual(result.id, layout.id, 'Should have id field'); - ok(Array.isArray(result.labels), 'Should have labels field as array'); - strictEqual(result.labels.length, 0, 'Should have empty labels array'); - ok(!result.name, 'Should not have name field'); - ok(!result.tabs, 'Should not have tabs field'); - }); - - test('should not mutate the original layout object', () => { - const layout = { - id: '7', - name: 'Original Layout', - owner_id: 'user7', - tabs: [ - { - name: 'Tab1', - objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], - }, - ], - }; - const fields = ['id', 'name', 'labels']; - const originalTabsReference = layout.tabs; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - ok(!('labels' in layout), 'Original layout should not have labels property added'); - strictEqual(layout.tabs, originalTabsReference, 'Original layout tabs should not be mutated'); - strictEqual(layout.owner_id, 'user7', 'Original layout owner_id should be unchanged'); - ok(result.labels, 'Result should have labels property'); - strictEqual(Object.keys(result).length, 3, 'Result should have only requested fields'); - }); - - test('should correctly trim complex layout with multiple tabs and objects', () => { - const layout = { - id: '8', - name: 'Complex Layout', - owner_id: 'user8', - createdAt: '2026-01-17', - updatedAt: '2026-01-17', - tabs: [ - { - name: 'Tab1', - objects: [ - { id: 'obj1', name: 'qc/MCH/QO/Check1' }, - { id: 'obj2', name: 'qc/TPC/QO/Check2' }, - ], - }, - { - name: 'Tab2', - objects: [{ id: 'obj3', name: 'daq/System/Monitor' }], - }, - ], - }; - const fields = ['id', 'name', 'labels', 'owner_id']; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - strictEqual(Object.keys(result).length, 4, 'Should have exactly 4 fields'); - strictEqual(result.id, layout.id); - strictEqual(result.name, layout.name); - strictEqual(result.owner_id, layout.owner_id); - ok(Array.isArray(result.labels), 'Labels should be an array'); - strictEqual(result.labels.length, 2, 'Should have 2 unique labels'); - ok(result.labels.includes('qc'), 'Should include "qc" label'); - ok(result.labels.includes('daq'), 'Should include "daq" label'); - ok(!result.tabs, 'Should not include tabs field'); - ok(!result.createdAt, 'Should not include createdAt field'); - ok(!result.updatedAt, 'Should not include updatedAt field'); - }); - - test('should handle single field selection', () => { - const layout = { - id: '9', - name: 'Single Field Layout', - owner_id: 'user9', - tabs: [ - { - name: 'Tab1', - objects: [{ id: 'obj1', name: 'qc/MCH/QO/Check1' }], - }, - ], - }; - const fields = ['name']; - - const result = layoutRepository._trimAndLabelLayout(layout, fields); - - strictEqual(Object.keys(result).length, 1, 'Should have exactly 1 field'); - strictEqual(result.name, layout.name, 'Should have correct name value'); - ok(!result.id, 'Should not have id field'); - ok(!result.owner_id, 'Should not have owner_id field'); - ok(!result.labels, 'Should not have labels field'); - ok(!result.tabs, 'Should not have tabs field'); - }); - }); }); }; diff --git a/QualityControl/test/test-index.js b/QualityControl/test/test-index.js index cffd93b55..393d00184 100644 --- a/QualityControl/test/test-index.js +++ b/QualityControl/test/test-index.js @@ -43,6 +43,8 @@ import { aboutPageTests } from './public/pages/about-page.test.js'; */ import { errorHandlerTestSuite } from './lib/utils/errorHandler.test.js'; import { httpRequestsTestSuite } from './lib/utils/httpRequests.test.js'; +import { addLabelsToLayoutTestSuite } from './lib/utils/layout/addLabelsToLayout.test.js'; +import { trimLayoutPerRequiredFieldsTestSuite } from './lib/utils/layout/trimLayoutPerRequiredFields.test.js'; /** * Controllers @@ -233,6 +235,8 @@ suite('All Tests - QCG', { timeout: FRONT_END_TIMEOUT + BACK_END_TIMEOUT }, asyn 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', () => { From cf57bafe7bdd86d56c22ba21a524596785fe55bb Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 13:54:19 +0100 Subject: [PATCH 06/10] Allow labels on layout put request --- QualityControl/lib/dtos/LayoutDto.js | 1 + QualityControl/lib/utils/layout/addLabelsToLayout.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/QualityControl/lib/dtos/LayoutDto.js b/QualityControl/lib/dtos/LayoutDto.js index 2b6b383ea..f7354f5fb 100644 --- a/QualityControl/lib/dtos/LayoutDto.js +++ b/QualityControl/lib/dtos/LayoutDto.js @@ -72,6 +72,7 @@ export const LayoutDto = Joi.object({ id: Joi.string().required(), name: Joi.string().min(3).max(40).required(), tabs: Joi.array().min(1).max(45).items(TabsDto).required(), + labels: Joi.array().items(Joi.string()).default([]), owner_id: Joi.number().min(0).required(), owner_name: Joi.string().required(), description: Joi.string().min(0).max(100).optional(), diff --git a/QualityControl/lib/utils/layout/addLabelsToLayout.js b/QualityControl/lib/utils/layout/addLabelsToLayout.js index 5541f3730..e2cf1f196 100644 --- a/QualityControl/lib/utils/layout/addLabelsToLayout.js +++ b/QualityControl/lib/utils/layout/addLabelsToLayout.js @@ -14,7 +14,7 @@ /** * Method to identify the unique prefix (encountering first '/') of objects and add it as a set of labels to layout * @param {LayoutDto} layout - layout object to which labels will be added - * @returns {{...LayoutDto, labels: string[]}} - layout object with added labels + * @returns {{LayoutDto, labels: string[]}} - layout object with added labels */ export const addLabelsToLayout = (layout) => { const labelsSet = new Set(); From 8e3a37bd15e955b46bae8535786d367f56a7ee9d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 14:09:26 +0100 Subject: [PATCH 07/10] Fix update layout via repository --- QualityControl/lib/repositories/LayoutRepository.js | 13 ++++++++++--- .../test/lib/repositories/LayoutRepository.test.js | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/QualityControl/lib/repositories/LayoutRepository.js b/QualityControl/lib/repositories/LayoutRepository.js index efe966d1e..bf907ce26 100644 --- a/QualityControl/lib/repositories/LayoutRepository.js +++ b/QualityControl/lib/repositories/LayoutRepository.js @@ -129,9 +129,13 @@ export class LayoutRepository extends BaseRepository { * @param {string} layoutId - id of the layout to be updated * @param {LayoutDto} newData - layout new data * @returns {string} id of the layout updated + * @throws {NotFoundError} - if the layout is not found */ async updateLayout(layoutId, newData) { - const layout = this.readLayoutById(layoutId); + const layout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId); + if (!layout) { + throw new NotFoundError(`layout (${layoutId}) not found`); + } Object.assign(layout, newData); await this._jsonFileService.writeToFile(); return layoutId; @@ -141,10 +145,13 @@ export class LayoutRepository extends BaseRepository { * Delete a single layout by its id * @param {string} layoutId - id of the layout to be removed * @returns {string} id of the layout deleted + * @throws {NotFoundError} - if the layout is not found */ async deleteLayout(layoutId) { - const layout = this.readLayoutById(layoutId); - const index = this._jsonFileService.data.layouts.indexOf(layout); + const index = this._jsonFileService.data.layouts.findIndex((layout) => layout.id === layoutId); + if (index === -1) { + throw new NotFoundError(`layout (${layoutId}) not found`); + } this._jsonFileService.data.layouts.splice(index, 1); await this._jsonFileService.writeToFile(); return layoutId; diff --git a/QualityControl/test/lib/repositories/LayoutRepository.test.js b/QualityControl/test/lib/repositories/LayoutRepository.test.js index 383ab262f..c3a84aade 100644 --- a/QualityControl/test/lib/repositories/LayoutRepository.test.js +++ b/QualityControl/test/lib/repositories/LayoutRepository.test.js @@ -196,6 +196,12 @@ export const layoutRepositoryTest = async () => { const idOfLayoutUpdated = await layoutRepository.updateLayout(idOfLayoutToUpdate, newLayout); strictEqual(idOfLayoutUpdated, idOfLayoutToUpdate); + const updatedLayout = addLabelsToLayout(jsonFileServiceMock.data.layouts.find((l) => l.id === newLayout.id)); + strictEqual(updatedLayout.id, newLayout.id); + strictEqual(updatedLayout.name, newLayout.name); + strictEqual(updatedLayout.owner_id, newLayout.owner_id); + deepStrictEqual(updatedLayout.tabs, newLayout.tabs); + sinon.assert.calledOnce(jsonFileServiceMock.writeToFile); }); }); From 50bea604192f1644323b0bbcb0893533a031a211 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 14:14:34 +0100 Subject: [PATCH 08/10] Do not update layout with labels field --- QualityControl/lib/repositories/LayoutRepository.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/QualityControl/lib/repositories/LayoutRepository.js b/QualityControl/lib/repositories/LayoutRepository.js index bf907ce26..2aa30c1fa 100644 --- a/QualityControl/lib/repositories/LayoutRepository.js +++ b/QualityControl/lib/repositories/LayoutRepository.js @@ -132,6 +132,9 @@ export class LayoutRepository extends BaseRepository { * @throws {NotFoundError} - if the layout is not found */ async updateLayout(layoutId, newData) { + if (newData.labels) { + delete newData.labels; + } const layout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId); if (!layout) { throw new NotFoundError(`layout (${layoutId}) not found`); From 063c6786947176483e196601a75dc1d7c12a8242 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 14:22:02 +0100 Subject: [PATCH 09/10] Add labels as requested fields --- QualityControl/public/common/RequestFields.enum.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/public/common/RequestFields.enum.js b/QualityControl/public/common/RequestFields.enum.js index 8f9b95e45..81b28ec5b 100644 --- a/QualityControl/public/common/RequestFields.enum.js +++ b/QualityControl/public/common/RequestFields.enum.js @@ -1,3 +1,3 @@ export const RequestFields = Object.freeze({ - LAYOUT_CARD: 'id,name,owner_id,owner_name,description,isOfficial', + LAYOUT_CARD: 'id,name,owner_id,owner_name,description,isOfficial,labels', }); From afe31a086e0b3e465b3709d929b2cff33fea5da9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 14:41:51 +0100 Subject: [PATCH 10/10] Update tests --- QualityControl/lib/repositories/LayoutRepository.js | 1 + .../test/lib/controllers/LayoutController.test.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/QualityControl/lib/repositories/LayoutRepository.js b/QualityControl/lib/repositories/LayoutRepository.js index 2aa30c1fa..5baa972aa 100644 --- a/QualityControl/lib/repositories/LayoutRepository.js +++ b/QualityControl/lib/repositories/LayoutRepository.js @@ -133,6 +133,7 @@ export class LayoutRepository extends BaseRepository { */ async updateLayout(layoutId, newData) { if (newData.labels) { + // labels are retrieved on front-end and might be send as PATCH/PUT if forgotten by developer delete newData.labels; } const layout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId); diff --git a/QualityControl/test/lib/controllers/LayoutController.test.js b/QualityControl/test/lib/controllers/LayoutController.test.js index f6411ce91..2c3265a2e 100644 --- a/QualityControl/test/lib/controllers/LayoutController.test.js +++ b/QualityControl/test/lib/controllers/LayoutController.test.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { ok, throws, doesNotThrow, AssertionError } from 'node:assert'; +import { ok, deepStrictEqual, throws, doesNotThrow, AssertionError } from 'node:assert'; import { suite, test, beforeEach } from 'node:test'; import sinon from 'sinon'; @@ -416,6 +416,7 @@ export const layoutControllerTestSuite = async () => { tabs: [{ name: 'tab', id: '1', columns: 2, objects: [] }], owner_id: 1, owner_name: 'one', + labels: [], collaborators: [], displayTimestamp: false, autoTabChange: 0, @@ -467,6 +468,7 @@ export const layoutControllerTestSuite = async () => { tabs: [{ name: 'tab', id: '1', columns: 2, objects: [] }], owner_id: 1, owner_name: 'one', + labels: [], collaborators: [], displayTimestamp: false, autoTabChange: 0, @@ -643,6 +645,7 @@ export const layoutControllerTestSuite = async () => { owner_name: 'admin', tabs: [{ id: '123', name: 'tab', columns: 2, objects: [] }], collaborators: [], + labels: [], displayTimestamp: false, autoTabChange: 0, }; @@ -671,6 +674,7 @@ export const layoutControllerTestSuite = async () => { owner_id: 1, owner_name: 'admin', tabs: [{ id: '123', name: 'tab', columns: 2, objects: [] }], + labels: [], collaborators: [], displayTimestamp: false, autoTabChange: 0, @@ -682,7 +686,10 @@ export const layoutControllerTestSuite = async () => { status: 500, title: 'Unknown Error', }), 'DataConnector error message is incorrect'); - ok(jsonStub.createLayout.calledWith(expected), 'New layout body was not used in data connector call'); + + // Log what was actually called for debugging + const actualCall = jsonStub.createLayout.getCall(0)?.args[0]; + deepStrictEqual(expected, actualCall); }); });