From 024ce1879056955af1661458c81a0daebe6b6c1a Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:22:10 +0100 Subject: [PATCH 1/7] Add a quick (and hacky) workaround for local storage reset due to non-isolated tests. --- QualityControl/test/public/features/filterTest.test.js | 6 ++++++ QualityControl/test/public/pages/object-tree.test.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/QualityControl/test/public/features/filterTest.test.js b/QualityControl/test/public/features/filterTest.test.js index 23c34d1df..f8e36b6dd 100644 --- a/QualityControl/test/public/features/filterTest.test.js +++ b/QualityControl/test/public/features/filterTest.test.js @@ -235,6 +235,12 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { }); await testParent.test('ObjectTreePage should apply filters for the objects', { timeout }, async () => { + // Ideally, tests should be isolated and not depend on each other. + // Currently, some tests rely on shared localStorage or page state changes from previous tests. + // As a workaround, we do targeted cleanup here to prevent issues in later tests. + const personid = await page.evaluate(() => window.model.session.personid); + await removeLocalStorage(page, `${StorageKeysEnum.OBJECT_TREE_OPEN_NODES}-${personid}`); + await page.goto( `${url}?page=objectTree`, { waitUntil: 'networkidle0' }, diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index cbd2c6ecb..e1e43e8d2 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -141,6 +141,12 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) 'should maintain panel width from localStorage on page reload', { timeout }, async () => { + // Ideally, tests should be isolated and not depend on each other. + // Currently, some tests rely on shared localStorage or page state changes from previous tests. + // As a workaround, we do targeted cleanup here to prevent issues in later tests. + const personid = await page.evaluate(() => window.model.session.personid); + await removeLocalStorage(page, `${StorageKeysEnum.OBJECT_TREE_OPEN_NODES}-${personid}`); + const dragAmount = 35; await page.reload({ waitUntil: 'networkidle0' }); await page.evaluate(() => document.querySelector('tr.object-selectable:nth-child(4)').click()); From b04a8bc964ce0299aa6173ef18265670fa2bf96e Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:22:39 +0100 Subject: [PATCH 2/7] Add a test to make sure the local storage is updated upon clicking a tree node element --- .../test/public/pages/object-tree.test.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index e1e43e8d2..bb71d2b55 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -43,6 +43,30 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) ok(rowsCount > 1); // more than 1 object in the tree }); + await testParent.test('should update local storage when tree node is clicked', { timeout }, async () => { + const selector = 'section > div > div > div > table > tbody > tr:nth-child(2)'; + const personid = await page.evaluate(() => window.model.session.personid); + const storageKey = `${StorageKeysEnum.OBJECT_TREE_OPEN_NODES}-${personid}`; + + await page.locator(selector).click(); + const localStorageBefore = await getLocalStorageAsJson(page, storageKey); + + await page.locator(selector).click(); + const localStorageAfter = await getLocalStorageAsJson(page, storageKey); + + // Ideally, tests should be isolated and not depend on each other. + // Currently, some tests rely on shared localStorage or page state changes from previous tests. + // As a workaround, we do targeted cleanup here to prevent issues in later tests. + await removeLocalStorage(page, storageKey); + await page.reload({ waitUntil: 'networkidle0' }); + + notDeepStrictEqual( + localStorageBefore, + localStorageAfter, + 'local storage should have changed after clicking a tree node', + ); + }); + await testParent.test('should preserve state if refreshed', { timeout }, async () => { const selector = 'section > div > div > div > table > tbody > tr:nth-child(2)'; await page.locator(selector).click(); From 01383973580121aad86d7df8fa7d9e68cb09ef78 Mon Sep 17 00:00:00 2001 From: hehoon <100522372+hehoon@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:40:19 +0100 Subject: [PATCH 3/7] Refactor tests --- .../test/public/features/filterTest.test.js | 6 ---- .../test/public/pages/object-tree.test.js | 30 ------------------- 2 files changed, 36 deletions(-) diff --git a/QualityControl/test/public/features/filterTest.test.js b/QualityControl/test/public/features/filterTest.test.js index f8e36b6dd..23c34d1df 100644 --- a/QualityControl/test/public/features/filterTest.test.js +++ b/QualityControl/test/public/features/filterTest.test.js @@ -235,12 +235,6 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { }); await testParent.test('ObjectTreePage should apply filters for the objects', { timeout }, async () => { - // Ideally, tests should be isolated and not depend on each other. - // Currently, some tests rely on shared localStorage or page state changes from previous tests. - // As a workaround, we do targeted cleanup here to prevent issues in later tests. - const personid = await page.evaluate(() => window.model.session.personid); - await removeLocalStorage(page, `${StorageKeysEnum.OBJECT_TREE_OPEN_NODES}-${personid}`); - await page.goto( `${url}?page=objectTree`, { waitUntil: 'networkidle0' }, diff --git a/QualityControl/test/public/pages/object-tree.test.js b/QualityControl/test/public/pages/object-tree.test.js index bb71d2b55..cbd2c6ecb 100644 --- a/QualityControl/test/public/pages/object-tree.test.js +++ b/QualityControl/test/public/pages/object-tree.test.js @@ -43,30 +43,6 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) ok(rowsCount > 1); // more than 1 object in the tree }); - await testParent.test('should update local storage when tree node is clicked', { timeout }, async () => { - const selector = 'section > div > div > div > table > tbody > tr:nth-child(2)'; - const personid = await page.evaluate(() => window.model.session.personid); - const storageKey = `${StorageKeysEnum.OBJECT_TREE_OPEN_NODES}-${personid}`; - - await page.locator(selector).click(); - const localStorageBefore = await getLocalStorageAsJson(page, storageKey); - - await page.locator(selector).click(); - const localStorageAfter = await getLocalStorageAsJson(page, storageKey); - - // Ideally, tests should be isolated and not depend on each other. - // Currently, some tests rely on shared localStorage or page state changes from previous tests. - // As a workaround, we do targeted cleanup here to prevent issues in later tests. - await removeLocalStorage(page, storageKey); - await page.reload({ waitUntil: 'networkidle0' }); - - notDeepStrictEqual( - localStorageBefore, - localStorageAfter, - 'local storage should have changed after clicking a tree node', - ); - }); - await testParent.test('should preserve state if refreshed', { timeout }, async () => { const selector = 'section > div > div > div > table > tbody > tr:nth-child(2)'; await page.locator(selector).click(); @@ -165,12 +141,6 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent) 'should maintain panel width from localStorage on page reload', { timeout }, async () => { - // Ideally, tests should be isolated and not depend on each other. - // Currently, some tests rely on shared localStorage or page state changes from previous tests. - // As a workaround, we do targeted cleanup here to prevent issues in later tests. - const personid = await page.evaluate(() => window.model.session.personid); - await removeLocalStorage(page, `${StorageKeysEnum.OBJECT_TREE_OPEN_NODES}-${personid}`); - const dragAmount = 35; await page.reload({ waitUntil: 'networkidle0' }); await page.evaluate(() => document.querySelector('tr.object-selectable:nth-child(4)').click()); From 650a859b8fdbed72516744efc5150a0ff0490608 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Tue, 6 Jan 2026 15:57:32 +0100 Subject: [PATCH 4/7] Move scroll on parent to help Mithril re-render --- QualityControl/public/common/header.js | 4 +- .../public/object/objectTreePage.js | 11 +++-- .../public/pages/aboutView/AboutViewPage.js | 22 ++++++--- .../pages/layoutListView/LayoutListPage.js | 48 ++++++++----------- QualityControl/public/view.js | 10 ++-- 5 files changed, 51 insertions(+), 44 deletions(-) diff --git a/QualityControl/public/common/header.js b/QualityControl/public/common/header.js index d965a4e59..ce4746f95 100644 --- a/QualityControl/public/common/header.js +++ b/QualityControl/public/common/header.js @@ -33,9 +33,9 @@ import { filtersPanel } from './filters/filterViews.js'; export default (model) => { const specific = headerSpecific(model) || {}; const { centerCol, rightCol, subRow } = specific; - + const id = `qcg-header-${model.page}`; return h('.flex-col', [ - h('.flex-row.p2.items-center', { id: 'qcg-header' }, [ + h('.flex-row.p2.items-center', { id, key: id }, [ commonHeader(model), centerCol || h('.flex-grow'), rightCol || h('.w-25'), diff --git a/QualityControl/public/object/objectTreePage.js b/QualityControl/public/object/objectTreePage.js index e3596cfe5..7d6fd3e1c 100644 --- a/QualityControl/public/object/objectTreePage.js +++ b/QualityControl/public/object/objectTreePage.js @@ -28,11 +28,14 @@ import { resizableDivider } from '../common/resizableDivider.js'; * @returns {vnode} - virtual node element */ export default (model) => { - const { object, router } = model; + const { object } = model; const { leftPanelWidthPercent } = object; - return h('.h-100.flex-column', { key: `${router.params.page}` }, [ - h('.flex-row.flex-grow', [ - h('.scroll-y.flex-column', { + return h('.flex-column.h-100', { + key: 'object-tree-page-container', + }, [ + h('.flex-row', { style: 'flex-grow: 1; height: 0;' }, [ + h('.flex-column.scroll-y', { + key: 'object-tree-scroll-container', style: { width: object.selected ? `${leftPanelWidthPercent}%` : '100%', }, diff --git a/QualityControl/public/pages/aboutView/AboutViewPage.js b/QualityControl/public/pages/aboutView/AboutViewPage.js index c640ece84..f10a9e556 100644 --- a/QualityControl/public/pages/aboutView/AboutViewPage.js +++ b/QualityControl/public/pages/aboutView/AboutViewPage.js @@ -19,12 +19,20 @@ import { h } from '/js/src/index.js'; /** * Shows a page to view framework information - * @param {Model} model - root model of the application + * @param {AboutViewModel} aboutViewModel - root model of the application * @returns {vnode} - virtual node element */ -export default (model) => h( - '.p2.absolute-fill.text-center', - servicesLoadingPanel(model.aboutViewModel.services[ServiceStatus.LOADING]), - servicesResolvedPanel(model.aboutViewModel.services[ServiceStatus.ERROR], 'error'), - servicesResolvedPanel(model.aboutViewModel.services[ServiceStatus.SUCCESS], 'success'), -); +export default (aboutViewModel) => { + const { services } = aboutViewModel; + return [ + h( + '.flex-column.flex-grow.p2.text-center', + { key: 'about-view-page' }, + [ + servicesLoadingPanel(services[ServiceStatus.LOADING]), + servicesResolvedPanel(services[ServiceStatus.ERROR], 'error'), + servicesResolvedPanel(services[ServiceStatus.SUCCESS], 'success'), + ], + ), + ]; +}; diff --git a/QualityControl/public/pages/layoutListView/LayoutListPage.js b/QualityControl/public/pages/layoutListView/LayoutListPage.js index 996d63adc..12ec30c57 100644 --- a/QualityControl/public/pages/layoutListView/LayoutListPage.js +++ b/QualityControl/public/pages/layoutListView/LayoutListPage.js @@ -23,33 +23,25 @@ import { filtersPanelPopover } from './filtersPanelPopover.js'; * @import LayoutListModel from './model/LayoutListModel.js'; */ export default (layoutListModel) => [ - h('.scroll-y.absolute-fill', [ - h( - '.flex-row.text-right.m2', - [ - filtersPanelPopover(layoutListModel.searchFilterModel), - h( - 'input.form-control.form-inline.mh1.w-33', - { - placeholder: 'Layout name', - type: 'text', - value: layoutListModel.searchFilterModel.searchInput, - oninput: (e) => { - layoutListModel.search(e.target.value); - }, - }, - ), - h('.p1', [ - h( - '.mh1', - layoutListModel.searchFilterModel.stringifyActiveFiltersFriendly(), - ), - ]), - ], - ), - - h('', { - style: 'display: flex; flex-direction: column', - }, Array.from(layoutListModel.folders.values()).map(FolderComponent)), + h('.flex-row.text-right.m2', [ + filtersPanelPopover(layoutListModel.searchFilterModel), + h('input.form-control.form-inline.mh1.w-33', { + placeholder: 'Layout name', + type: 'text', + value: layoutListModel.searchFilterModel.searchInput, + oninput: (e) => { + layoutListModel.search(e.target.value); + }, + }), + h('.p1', [ + h( + '.mh1', + layoutListModel.searchFilterModel.stringifyActiveFiltersFriendly(), + ), + ]), ]), + + h('', { + key: 'layout-list-page-folders-container', + }, Array.from(layoutListModel.folders.values()).map(FolderComponent)), ]; diff --git a/QualityControl/public/view.js b/QualityControl/public/view.js index ac3418cfe..e6079abaa 100644 --- a/QualityControl/public/view.js +++ b/QualityControl/public/view.js @@ -36,9 +36,13 @@ export default (model) => [ model.isImportVisible && layoutImportModal(model), h('.absolute-fill.flex-column', [ h('header.shadow-level2.level2', [header(model)]), - h('.flex-grow.flex-row.outline-gray', [ + h('.flex-row.flex-grow', { + key: 'main-content', + }, [ sidebar(model), - h('section.outline-gray.flex-grow.relative', page(model)), + h('section', { + style: 'flex-grow: 1; position: relative; overflow: auto;', + }, page(model)), ]), ]), notification(model.notification), @@ -55,7 +59,7 @@ function page(model) { case 'layoutShow': return layoutViewPage(model); case 'objectTree': return objectTreePage(model); case 'objectView': return ObjectViewPage(model.objectViewModel); - case 'about': return AboutViewPage(model); + case 'about': return AboutViewPage(model.aboutViewModel); // Should be seen only at the first start when the view is not yet really to be shown (data loading) default: return null; From d625d2c0abb21f46cdc24d9b0685f8021ef98dc6 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Wed, 7 Jan 2026 10:36:47 +0100 Subject: [PATCH 5/7] Update base path for layout-list --- QualityControl/test/public/pages/layout-list.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QualityControl/test/public/pages/layout-list.test.js b/QualityControl/test/public/pages/layout-list.test.js index 8308a6013..cce92363e 100644 --- a/QualityControl/test/public/pages/layout-list.test.js +++ b/QualityControl/test/public/pages/layout-list.test.js @@ -30,7 +30,7 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent) const allLayoutIndex = 2; const allLayoutIndex2 = 3; - const basePath = (index) => `section > div > div:nth-child(${index})`; + const basePath = (index) => `section > div:nth-child(${index})`; const toggleFolderPath = (index, index2) => index2 ? `${basePath(index)} > div:nth-child(${index2}) > div > b` : `${basePath(index)} div > b`; const cardPath = (index, cardIndex) => `${basePath(index)} .card:nth-child(${cardIndex})`; From 8d5d8b933046d2f5c52a4247f7c7283c7df43f81 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 8 Jan 2026 10:22:40 +0100 Subject: [PATCH 6/7] Fix tests following changes in UI --- QualityControl/test/public/features/filterTest.test.js | 2 +- QualityControl/test/public/pages/layout-list.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/QualityControl/test/public/features/filterTest.test.js b/QualityControl/test/public/features/filterTest.test.js index 23c34d1df..625a1bec7 100644 --- a/QualityControl/test/public/features/filterTest.test.js +++ b/QualityControl/test/public/features/filterTest.test.js @@ -254,7 +254,7 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { }); await testParent.test('ObjectTree infoPanel should show filtered object versions', { timeout }, async () => { - const versionsPath = '.outline-gray.flex-grow.relative select option'; + const versionsPath = 'section select option'; await page.locator('tr:last-of-type td').click(); await page.waitForSelector(versionsPath); diff --git a/QualityControl/test/public/pages/layout-list.test.js b/QualityControl/test/public/pages/layout-list.test.js index cce92363e..d3e236fbe 100644 --- a/QualityControl/test/public/pages/layout-list.test.js +++ b/QualityControl/test/public/pages/layout-list.test.js @@ -37,7 +37,7 @@ export const layoutListPageTests = async (url, page, timeout = 5000, testParent) const cardLayoutLinkPath = (cardPath) => `${cardPath} a`; const cardOfficialButtonPath = (cardPath) => `${cardPath} > .cardHeader > button`; - const filterPath = 'section > div > div:nth-child(1) > input'; + const filterPath = 'section > div > input'; const filterObjectPath = 'input.form-control:nth-child(1)'; await testParent.test('should not show a download button when there is no data', async () => { await page.goto(`${url}?page=layoutShow&layoutId=671b8c22402408122e2f20dd&tab=main`, { waitUntil: 'networkidle0' }); From 6501036f63c25321d28e6476f9732ffb48d340a6 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 8 Jan 2026 10:23:00 +0100 Subject: [PATCH 7/7] Add timeouts as per tests --- QualityControl/test/test-index.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/QualityControl/test/test-index.js b/QualityControl/test/test-index.js index 40412d607..75514eb4b 100644 --- a/QualityControl/test/test-index.js +++ b/QualityControl/test/test-index.js @@ -104,12 +104,14 @@ const FRONT_END_PER_TEST_TIMEOUT = 5000; // each front-end test is allowed this const INITIAL_PAGE_SETUP_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 5; const QC_DRAWING_OPTIONS_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 13; -const LAYOUT_LIST_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 6; -const OBJECT_TREE_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 6; +const LAYOUT_LIST_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 17; +const OBJECT_TREE_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 20; const OBJECT_VIEW_FROM_OBJECT_TREE_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 5; -const OBJECT_VIEW_FROM_LAYOUT_SHOW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 4; +const OBJECT_VIEW_FROM_LAYOUT_SHOW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 17; const LAYOUT_SHOW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 23; const ABOUT_VIEW_PAGE_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 4; +const FILTER_TEST_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 26; +const RUN_MODE_TEST_TIMEOUT = FRONT_END_PER_TEST_TIMEOUT * 10; const FRONT_END_TIMEOUT = INITIAL_PAGE_SETUP_TIMEOUT + QC_DRAWING_OPTIONS_TIMEOUT @@ -117,7 +119,10 @@ const FRONT_END_TIMEOUT = INITIAL_PAGE_SETUP_TIMEOUT + OBJECT_TREE_PAGE_TIMEOUT + OBJECT_VIEW_FROM_OBJECT_TREE_PAGE_TIMEOUT + OBJECT_VIEW_FROM_LAYOUT_SHOW_PAGE_TIMEOUT - + LAYOUT_SHOW_PAGE_TIMEOUT; + + LAYOUT_SHOW_PAGE_TIMEOUT + + ABOUT_VIEW_PAGE_TIMEOUT + + FILTER_TEST_TIMEOUT + + RUN_MODE_TEST_TIMEOUT; const BACK_END_TIMEOUT = 10000; // back-end test suite timeout @@ -181,11 +186,17 @@ suite('All Tests - QCG', { timeout: FRONT_END_TIMEOUT + BACK_END_TIMEOUT }, asyn { timeout: ABOUT_VIEW_PAGE_TIMEOUT }, async (testParent) => await aboutPageTests(url, page, FRONT_END_PER_TEST_TIMEOUT, testParent), ); - test('should successfully import and run tests for filter', async (testParent) => - await filterTests(url, page, FRONT_END_PER_TEST_TIMEOUT, testParent)); + test( + 'should successfully import and run tests for filter', + { timeout: FILTER_TEST_TIMEOUT }, + async (testParent) => await filterTests(url, page, FRONT_END_PER_TEST_TIMEOUT, testParent), + ); - test('should successfully use run mode when available', async (testParent) => - await runModeTests(url, page, FRONT_END_PER_TEST_TIMEOUT, testParent)); + test( + 'should successfully use run mode when available', + { timeout: RUN_MODE_TEST_TIMEOUT }, + async (testParent) => await runModeTests(url, page, FRONT_END_PER_TEST_TIMEOUT, testParent), + ); }); suite('API - test suite', { timeout: FRONT_END_TIMEOUT }, async () => {