From b2ac70c4f3f472e8a21a6a1620b163acef9862f0 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:16:15 +0100 Subject: [PATCH 1/7] Fix out of bounds object not rendering until edit mode is entered once --- QualityControl/public/layout/Layout.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/QualityControl/public/layout/Layout.js b/QualityControl/public/layout/Layout.js index 81c805581..325128823 100644 --- a/QualityControl/public/layout/Layout.js +++ b/QualityControl/public/layout/Layout.js @@ -287,9 +287,7 @@ export default class Layout extends BaseViewModel { this.gridListSize = parseInt(value, 10); this.cellHeight = 100 / this.gridListSize * 0.95; // %, put some margin at bottom to see below this.cellWidth = 100 / this.gridListSize; // % - if (this.editEnabled) { - this.gridList.resizeGrid(this.gridListSize); - } + this.gridList.resizeGrid(this.gridListSize); this.tab.columns = this.gridListSize; this.tab.objects.forEach((object) => { if (object.w > this.tab.columns) { From be2e1f00e90a4544d314c415826d41d7da856f17 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:39:22 +0100 Subject: [PATCH 2/7] Fix out of bounds object not rendering on refresh or on JSON edit menu apply when reducing the column count --- QualityControl/public/layout/Layout.js | 10 ++-- .../test/public/pages/layout-show.test.js | 51 ++++++++++++++++- .../seeders/layout-show/json-file-mock.js | 55 +++++++++++++++++++ 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/QualityControl/public/layout/Layout.js b/QualityControl/public/layout/Layout.js index 325128823..bf3cc868d 100644 --- a/QualityControl/public/layout/Layout.js +++ b/QualityControl/public/layout/Layout.js @@ -261,6 +261,8 @@ export default class Layout extends BaseViewModel { const result = await this.model.services.layout.saveLayout(this.item); if (result.isSuccess()) { await this.model.services.layout.getLayoutsByUserId(this.model.session.personid); + this._tabIndex = this._tabIndex < this.item.tabs.length ? this._tabIndex : 0; + this.selectTab(this._tabIndex); this.model.notification.show(`Layout "${this.item.name}" has been saved successfully.`, 'success'); } else { this.item = this.editOriginalClone; @@ -287,7 +289,9 @@ export default class Layout extends BaseViewModel { this.gridListSize = parseInt(value, 10); this.cellHeight = 100 / this.gridListSize * 0.95; // %, put some margin at bottom to see below this.cellWidth = 100 / this.gridListSize; // % - this.gridList.resizeGrid(this.gridListSize); + if (this.editEnabled) { + this.gridList.resizeGrid(this.gridListSize); + } this.tab.columns = this.gridListSize; this.tab.objects.forEach((object) => { if (object.w > this.tab.columns) { @@ -304,9 +308,7 @@ export default class Layout extends BaseViewModel { */ sortObjectsOfCurrentTab() { this.gridList.items = this.tab.objects; - if (this.editEnabled) { - this.gridList.resizeGrid(this.gridListSize); - } + this.gridList.resizeGrid(this.gridListSize); } /** diff --git a/QualityControl/test/public/pages/layout-show.test.js b/QualityControl/test/public/pages/layout-show.test.js index 959e77995..249b33501 100644 --- a/QualityControl/test/public/pages/layout-show.test.js +++ b/QualityControl/test/public/pages/layout-show.test.js @@ -13,7 +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'; +import { editedMockedLayout, editedManyObjectsMockedLayout } from '../../setup/seeders/layout-show/json-file-mock.js'; import { getElementCenter } from '../../testUtils/dragAndDrop.js'; /** @@ -436,6 +436,49 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => }, ); + await testParent.test( + 'should display all objects even when the JSON has out of bound entries', + { timeout }, + async () => { + const EXPECTED_LAYOUT_OBJECT_COUNT = editedManyObjectsMockedLayout.tabs[0].objects.length; + const getRenderedObjectCount = async () => + await page.evaluate(() => document.querySelector('#subcanvas').children.length); + + // Ensure we are on the main tab + await page.locator('#tab-0').click(); + await delay(100); + + const pencilButtonPath = '.btn-group > div > button'; + await page.locator(pencilButtonPath).click(); + + const editViaJSONButtonPath = '#editByJson'; + await page.locator(editViaJSONButtonPath).click(); + + const textareaPath = '#layout-json-editor'; + const mockedJSON = JSON.stringify(editedManyObjectsMockedLayout); + await page.locator(textareaPath).fill(mockedJSON); + + const updateButtonPath = '#updateLayoutButton'; + await page.locator(updateButtonPath).click(); + await delay(100); + + strictEqual( + await getRenderedObjectCount(), + EXPECTED_LAYOUT_OBJECT_COUNT, + `Expected ${EXPECTED_LAYOUT_OBJECT_COUNT} rendered objects after JSON update`, + ); + + await page.reload({ waitUntil: 'networkidle0' }); + await delay(100); + + strictEqual( + await getRenderedObjectCount(), + EXPECTED_LAYOUT_OBJECT_COUNT, + `Expected ${EXPECTED_LAYOUT_OBJECT_COUNT} rendered objects after page reload`, + ); + }, + ); + await testParent.test( 'should update layout when clicking "Update layout"', { timeout }, @@ -464,9 +507,13 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => ); await testParent.test('should change tab after set tabInterval', { timeout: 15000 }, async () => { + // Ensure we are on the 'a' tab + await page.locator('#tab-1').click(); + await delay(100); + const location = await page.evaluate(() => window.location); strictEqual(location.search, `?page=layoutShow&layoutId=${LAYOUT_ID}&tab=a`); - await delay(11000); + await delay(11111); const location2 = await page.evaluate(() => window.location); strictEqual(location2.search, `?page=layoutShow&layoutId=${LAYOUT_ID}&tab=test`); }); diff --git a/QualityControl/test/setup/seeders/layout-show/json-file-mock.js b/QualityControl/test/setup/seeders/layout-show/json-file-mock.js index ac7e7b420..197be352d 100644 --- a/QualityControl/test/setup/seeders/layout-show/json-file-mock.js +++ b/QualityControl/test/setup/seeders/layout-show/json-file-mock.js @@ -30,3 +30,58 @@ export const editedMockedLayout = { displayTimestamp: false, autoTabChange: 11, }; + +export const editedManyObjectsMockedLayout = { + name: 'a-test', + tabs: [ + { + name: 'main', + objects: [ + { + x: 0, + y: 0, + h: 1, + w: 1, + name: 'qc/test/object/1', + options: [], + ignoreDefaults: false, + }, + { + x: 1, + y: 0, + h: 1, + w: 1, + name: 'qc/test/object/1', + options: [], + ignoreDefaults: false, + }, + { + x: 2, + y: 0, + h: 1, + w: 1, + name: 'qc/test/object/1', + options: [], + ignoreDefaults: false, + }, + { + x: 0, + y: 1, + h: 1, + w: 1, + name: 'qc/test/object/1', + options: [], + ignoreDefaults: false, + }, + ], + columns: 1, + }, + { + name: 'a', + objects: [], + columns: 1, + }, + ], + displayTimestamp: false, + autoTabChange: 11, +}; From df94f69bd1a3fe1b71fe695abf4efb1ca85315b6 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:43:01 +0100 Subject: [PATCH 3/7] Revert changes to the `sortObjectsOfCurrentTab` function --- QualityControl/public/layout/Layout.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/QualityControl/public/layout/Layout.js b/QualityControl/public/layout/Layout.js index bf3cc868d..7e1126c19 100644 --- a/QualityControl/public/layout/Layout.js +++ b/QualityControl/public/layout/Layout.js @@ -308,7 +308,9 @@ export default class Layout extends BaseViewModel { */ sortObjectsOfCurrentTab() { this.gridList.items = this.tab.objects; - this.gridList.resizeGrid(this.gridListSize); + if (this.editEnabled) { + this.gridList.resizeGrid(this.gridListSize); + } } /** From e8d7d3eb0314a01dfd4ee9f6ffa9865d0a8fa70e Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:31:10 +0100 Subject: [PATCH 4/7] Increase test timeout --- QualityControl/test/public/pages/layout-show.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/test/public/pages/layout-show.test.js b/QualityControl/test/public/pages/layout-show.test.js index 0db078e07..b69038526 100644 --- a/QualityControl/test/public/pages/layout-show.test.js +++ b/QualityControl/test/public/pages/layout-show.test.js @@ -448,7 +448,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => await testParent.test( 'should display all objects even when the JSON has out of bound entries', - { timeout }, + { timeout: 10000 }, async () => { const EXPECTED_LAYOUT_OBJECT_COUNT = editedManyObjectsMockedLayout.tabs[0].objects.length; const getRenderedObjectCount = async () => From 052c52d3f81bd83cb59428bb68defb11c59b8e78 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:39:11 +0100 Subject: [PATCH 5/7] Increase some more test timeouts --- QualityControl/test/public/pages/layout-show.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/QualityControl/test/public/pages/layout-show.test.js b/QualityControl/test/public/pages/layout-show.test.js index b69038526..9823f9af9 100644 --- a/QualityControl/test/public/pages/layout-show.test.js +++ b/QualityControl/test/public/pages/layout-show.test.js @@ -448,7 +448,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => await testParent.test( 'should display all objects even when the JSON has out of bound entries', - { timeout: 10000 }, + { timeout: 15000 }, async () => { const EXPECTED_LAYOUT_OBJECT_COUNT = editedManyObjectsMockedLayout.tabs[0].objects.length; const getRenderedObjectCount = async () => @@ -491,7 +491,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => await testParent.test( 'should update layout when clicking "Update layout"', - { timeout }, + { timeout: 10000 }, async () => { const pencilButtonPath = '.btn-group > div > button'; await page.locator(pencilButtonPath).click(); @@ -530,7 +530,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => await testParent.test( 'should update layout name in sidebar when name is changed and saved via JSON editor', - { timeout }, + { timeout: 10000 }, async () => { const originalSidebarName = await page.evaluate(() => { const sidebarLayoutLink = document.querySelector('nav a.menu-item.w-wrapped.selected span:nth-child(2)'); From b7634a38108667824fc126367f028ecb2da04212 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:07:53 +0100 Subject: [PATCH 6/7] Fix out-of-bounds layout object not rendering, now auto-places in available column --- QualityControl/public/layout/Layout.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/QualityControl/public/layout/Layout.js b/QualityControl/public/layout/Layout.js index 7e1126c19..e78d4b007 100644 --- a/QualityControl/public/layout/Layout.js +++ b/QualityControl/public/layout/Layout.js @@ -289,9 +289,7 @@ export default class Layout extends BaseViewModel { this.gridListSize = parseInt(value, 10); this.cellHeight = 100 / this.gridListSize * 0.95; // %, put some margin at bottom to see below this.cellWidth = 100 / this.gridListSize; // % - if (this.editEnabled) { - this.gridList.resizeGrid(this.gridListSize); - } + this.gridList.resizeGrid(this.gridListSize); this.tab.columns = this.gridListSize; this.tab.objects.forEach((object) => { if (object.w > this.tab.columns) { @@ -308,9 +306,7 @@ export default class Layout extends BaseViewModel { */ sortObjectsOfCurrentTab() { this.gridList.items = this.tab.objects; - if (this.editEnabled) { - this.gridList.resizeGrid(this.gridListSize); - } + this.gridList.resizeGrid(this.gridListSize); } /** From e61dd57468dc799248bf54ae22aa530da6fec125 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:09:31 +0100 Subject: [PATCH 7/7] Update test to count only visible objects within viewport instead of all rendered children --- .../test/public/pages/layout-show.test.js | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/QualityControl/test/public/pages/layout-show.test.js b/QualityControl/test/public/pages/layout-show.test.js index 9823f9af9..cc18f809a 100644 --- a/QualityControl/test/public/pages/layout-show.test.js +++ b/QualityControl/test/public/pages/layout-show.test.js @@ -13,7 +13,10 @@ import { strictEqual, ok, deepStrictEqual } from 'node:assert'; import { delay } from '../../testUtils/delay.js'; -import { editedMockedLayout, editedManyObjectsMockedLayout } from '../../setup/seeders/layout-show/json-file-mock.js'; +import { + editedManyObjectsMockedLayout, + editedMockedLayout, +} from '../../setup/seeders/layout-show/json-file-mock.js'; import { getElementCenter } from '../../testUtils/dragAndDrop.js'; /** @@ -187,7 +190,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => const leftStyle = await page.evaluate(() => document.querySelector('#subcanvas .dropdown-menu').style.left); strictEqual(leftStyle, '0.1em'); - } + }, ); await testParent.test('should have second tab to be empty (according to demo data)', { timeout }, async () => { @@ -349,7 +352,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => elements.map((element) => element.textContent.trim())); strictEqual(tabNames[1], originalTabNames[0]); - } + }, ); await testParent.test( @@ -451,8 +454,25 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => { timeout: 15000 }, async () => { const EXPECTED_LAYOUT_OBJECT_COUNT = editedManyObjectsMockedLayout.tabs[0].objects.length; - const getRenderedObjectCount = async () => - await page.evaluate(() => document.querySelector('#subcanvas').children.length); + + /** + * Counts the number of visible child elements within #subcanvas. + * Visibility is defined as being within the viewport. + * @returns {number} The amount of visible rendered objects + */ + const getVisibleObjectCount = async () => + await page.evaluate(() => { + const container = document.querySelector('#subcanvas'); + if (!container) { + return 0; + } + + // Count elements that intersect with the viewport + return Array.from(container.children).filter((child) => { + const rect = child.getBoundingClientRect(); + return rect.bottom >= 0 && rect.top >= 0 && rect.left >= 0 && rect.right <= window.innerWidth; + }).length; + }); // Ensure we are on the main tab await page.locator('#tab-0').click(); @@ -473,7 +493,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => await delay(100); strictEqual( - await getRenderedObjectCount(), + await getVisibleObjectCount(), EXPECTED_LAYOUT_OBJECT_COUNT, `Expected ${EXPECTED_LAYOUT_OBJECT_COUNT} rendered objects after JSON update`, ); @@ -482,7 +502,7 @@ export const layoutShowTests = async (url, page, timeout = 5000, testParent) => await delay(100); strictEqual( - await getRenderedObjectCount(), + await getVisibleObjectCount(), EXPECTED_LAYOUT_OBJECT_COUNT, `Expected ${EXPECTED_LAYOUT_OBJECT_COUNT} rendered objects after page reload`, );