diff --git a/test/public/defaults.js b/test/public/defaults.js index bb7ae4b811..d841c4bc05 100644 --- a/test/public/defaults.js +++ b/test/public/defaults.js @@ -143,21 +143,51 @@ module.exports.waitForTimeout = waitForTimeout; /** * Waits for the number of table rows to meet the expected size. * + * This function continuously polls the page until the table has exactly the expected number of rows, + * excluding rows with the CSS classes 'loading-row' or 'empty-row'. If an old table fingerprint is + * provided, it also verifies that the table content has actually changed before resolving. + * * @param {puppeteer.Page} page - The puppeteer page where the table is located. * @param {number} expectedSize - The expected number of table rows, excluding rows marked as loading or empty. - * @return {Promise} Resolves once the expected number of rows is met, or the timeout is reached. - */ -const waitForTableToLength = async (page, expectedSize) => { + * @param {number} [timeout] - Max wait time in ms; if omitted, uses the page default timeout. + * @param {string} [oldTableFingerprint] - HTML innerHTML of the previous table state. When provided, + * the function ensures the table content has changed even if the row + * count matches, preventing false positives during updates. + * @return {Promise} Resolves once the expected number of rows is met (and table content has changed if + * oldTableFingerprint was provided). + * @throws {Error} Throws an error if the timeout is reached before the condition is met. + */ +const waitForTableToLength = async (page, expectedSize, timeout, oldTableFingerprint) => { + const tableRowsSelector = 'table tbody tr:not(.loading-row):not(.empty-row)'; + const tableSelector = 'table'; try { await page.waitForFunction( - (expectedSize) => document.querySelectorAll('table tbody tr:not(.loading-row):not(.empty-row)').length === expectedSize, - {}, + (expectedSize, oldTableFingerprint, tableRowsSelector, tableSelector) => { + const actualSize = document.querySelectorAll(tableRowsSelector).length; + + if (actualSize === expectedSize) { + if (oldTableFingerprint) { + const currentTable = document.querySelector(tableSelector).innerHTML; + // Ensure table content has changed, useful if row count was the same before + return currentTable !== oldTableFingerprint; + } + return true; + } + return false; + }, + timeout ? { timeout } : undefined, expectedSize, + oldTableFingerprint, + tableRowsSelector, + tableSelector, ); } catch { - const actualSize = (await page.$$('tbody tr')).length; - const isThereLoadingRow = !!(await page.$$('table body tr.loading-row')) - throw new Error(`Expected table of length ${expectedSize}, but got ${actualSize} ${isThereLoadingRow ? ', loading-row' : ''}`); + // Gather information to provide helpful error message + const actualSize = (await page.$$(tableRowsSelector)).length; + const isThereLoadingRow = (await page.$$('table tbody tr.loading-row')).length > 0; + const currentTable = await page.$eval(tableSelector, (t) => t.innerHTML).catch(() => null); + const tableUnchanged = oldTableFingerprint && currentTable === oldTableFingerprint; + throw new Error(`Expected table of length ${expectedSize}, but got ${actualSize}${isThereLoadingRow ? ' (loading-row present)' : ''}${tableUnchanged ? ' (table content unchanged)' : ''}`); } }; @@ -941,4 +971,9 @@ module.exports.resetFilters = async (page) => { await this.pressElement(page, '#reset-filters', true); await this.pressElement(page, '#openFilterToggle', true); }); + + await page.waitForFunction( + () => document.querySelectorAll('table tbody tr.loading-row').length === 0, + { timeout: 5000 }, + ); }; diff --git a/test/public/envs/overview.test.js b/test/public/envs/overview.test.js index 1b9fa872c6..8fa2c38213 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -424,7 +424,7 @@ module.exports = () => { await fillInput(page, selector.fromDateSelector, fromDate, ['change']); await fillInput(page, selector.toDateSelector, toDate, ['change']); - await waitForTableLength(page, expectedIds.length); + await waitForTableLength(page, expectedIds.length, 5000); expect(await page.$$eval('tbody tr', (rows) => rows.map((row) => row.id))).to.eql(expectedIds.map(id => `row${id}`)); }; @@ -442,6 +442,7 @@ module.exports = () => { ['eZF99lH6'], ); await resetFilters(page); + await waitForTableLength(page, 9, 10000); await filterOnCreatedAt( periodInputsSelectors, @@ -452,5 +453,6 @@ module.exports = () => { ['GIDO1jdkD', '8E4aZTjY', 'Dxi029djX'], ); await resetFilters(page); + await waitForTableLength(page, 9, 10000); }); }; diff --git a/test/public/runs/detail.test.js b/test/public/runs/detail.test.js index 9d03403f40..fa94143746 100644 --- a/test/public/runs/detail.test.js +++ b/test/public/runs/detail.test.js @@ -521,8 +521,10 @@ module.exports = () => { it('should successfully display links to infologger and QCG', async () => { await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); + await page.waitForSelector('#row108 a'); await waitForNavigation(page, () => pressElement(page, '#row108 a')); + await page.waitForSelector('a.external-link'); await expectLink(page, 'a.external-link', { innerText: 'FLP', href: 'http://localhost:8081/?q={%22run%22:{%22match%22:%22108%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 14065478f7..5ceac005b2 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -255,10 +255,10 @@ module.exports = () => { it('can navigate to a run detail page', async () => { await navigateToRunsOverview(page); - await page.waitForSelector('tbody tr'); - const expectedRunNumber = await page.evaluate(() => document.querySelector('tbody tr:first-of-type a').innerText); + const firstLink = await page.waitForSelector('tbody tr:first-of-type a'); + const expectedRunNumber = await firstLink.evaluate((el) => el.innerText); - await waitForNavigation(page, () => page.evaluate(() => document.querySelector('tbody tr:first-of-type a').click())); + await waitForNavigation(page, () => firstLink.evaluate((el) => el.click())); const redirectedUrl = await page.url(); const urlParameters = redirectedUrl.slice(redirectedUrl.indexOf('?') + 1).split('&'); @@ -775,12 +775,14 @@ module.exports = () => { it('should successfully filter by EOR Reason types', async () => { // Expect the EOR filter to exist await page.waitForSelector('#eorCategories'); - const eorTitleDropdown = await page.waitForSelector('#eorTitles'); + await page.waitForSelector('#eorTitles'); // Select the EOR reason category DETECTORS + const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); await page.select('#eorCategories', 'DETECTORS'); - await waitForTableLength(page, 3); - let detectorTitleElements = await eorTitleDropdown.$$('option'); + await waitForTableLength(page, 3, undefined, oldTable); + await page.waitForSelector('#eorTitles option'); + let detectorTitleElements = await page.$$('#eorTitles option'); expect(detectorTitleElements).has.lengthOf(3); // The titles dropdown should have updated @@ -819,7 +821,7 @@ module.exports = () => { // Reset filters. There should be a single blank option in the EOR titles dropdown await resetFilters(page) await waitForTableLength(page, 10); - detectorTitleElements = await eorTitleDropdown.$$('option'); + detectorTitleElements = await page.$$('#eorTitles option'); expect(detectorTitleElements).has.lengthOf(1); // There should be many items in the run details table @@ -876,7 +878,7 @@ module.exports = () => { expect(exportModal).to.be.null; await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await page.waitForSelector('#export-data-modal'); + await page.waitForSelector('#export-data-modal', { timeout: 5000 }); exportModal = await page.$('#export-data-modal'); expect(exportModal).to.not.be.null; @@ -906,10 +908,10 @@ module.exports = () => { // First export await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true) - await page.waitForSelector('#export-data-modal'); + await page.waitForSelector('#export-data-modal', { timeout: 5000 }); await page.waitForSelector('#send:disabled'); - await page.waitForSelector('.form-control'); - await page.select('.form-control', 'runQuality', 'runNumber'); + await page.waitForSelector('#export-data-modal select.form-control'); + await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); await page.waitForSelector('#send:enabled'); const exportButtonText = await page.$eval('#send', (button) => button.innerText); expect(exportButtonText).to.be.eql('Export'); @@ -939,16 +941,15 @@ module.exports = () => { await openFilteringPanel(page);; await page.waitForSelector(badFilterSelector); await page.$eval(badFilterSelector, (element) => element.click()); - await page.waitForSelector('.atom-spinner'); await page.waitForSelector('tbody tr:nth-child(2)'); await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); ///// Download await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await page.waitForSelector('#export-data-modal'); + await page.waitForSelector('#export-data-modal', { timeout: 5000 }); - await page.waitForSelector('.form-control'); - await page.select('.form-control', 'runQuality', 'runNumber'); + await page.waitForSelector('#export-data-modal select.form-control', { timeout: 10000 }); + await page.select('#export-data-modal select.form-control', 'runQuality', 'runNumber'); { const downloadPath = await waitForDownload(page, () => pressElement(page, '#send:enabled', true)); diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 090384e0f6..e8ea387b87 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -189,15 +189,17 @@ module.exports = () => { }); it('should switch mcReproducibleAsNotBad', async () => { + let oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); await pressElement(page, '#mcReproducibleAsNotBadToggle input', true); - await waitForTableLength(page, 3); + await waitForTableLength(page, 3, 5000, oldTable); await expectInnerText(page, 'tr#row106 .column-CPV a', '89'); await expectLink(page, 'tr#row106 .column-CPV a', { href: 'http://localhost:4000/?page=qc-flags-for-data-pass&runNumber=106&dplDetectorId=1&dataPassId=1', innerText: '89', }); + oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); await pressElement(page, '#mcReproducibleAsNotBadToggle input', true); - await waitForTableLength(page, 3); + await waitForTableLength(page, 3, 5000, oldTable); await expectInnerText(page, 'tr#row106 .column-CPV a', '67MC.R'); await expectLink(page, 'tr#row106 .column-CPV a', { href: 'http://localhost:4000/?page=qc-flags-for-data-pass&runNumber=106&dplDetectorId=1&dataPassId=1', @@ -242,8 +244,9 @@ module.exports = () => { await pressElement(page, amountItems5); // Expect the amount of visible runs to reduce when the first option (5) is selected - await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); await waitForTableLength(page, 4); + await page.waitForSelector('.dropup button'); + await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); // Expect the custom per page input to have red border and text color if wrong value typed const customPerPageInput = await page.$(`${amountSelectorId} input[type=number]`); @@ -402,6 +405,7 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['106']); + await page.waitForSelector('#openFilterToggle'); await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); @@ -420,6 +424,7 @@ module.exports = () => { */ await page.select('.runDuration-filter select', '>='); await fillInput(page, '#duration-operand', '10', ['change']); + await waitForTableLength(page, 2); await expectColumnValues(page, 'runNumber', ['55', '1']); @@ -612,8 +617,9 @@ module.exports = () => { await pressElement(page, '#actions-dropdown-button .popover-trigger', true); setConfirmationDialogToBeAccepted(page); await pressElement(page, `${popoverSelector} button:nth-child(4)`, true); + const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); await pressElement(page, '#actions-dropdown-button .popover-trigger', true); - await waitForTableLength(page, 3); + await waitForTableLength(page, 3, undefined, oldTable); // Processing of data might take a bit of time, but then expect QC flag button to be there await expectInnerText( page, diff --git a/test/public/runs/runsPerLhcPeriod.overview.test.js b/test/public/runs/runsPerLhcPeriod.overview.test.js index 3a00301943..f38dc635a9 100644 --- a/test/public/runs/runsPerLhcPeriod.overview.test.js +++ b/test/public/runs/runsPerLhcPeriod.overview.test.js @@ -110,10 +110,13 @@ module.exports = () => { ...Object.fromEntries(DETECTORS.map((detectorName) => [detectorName, (quality) => expect(quality).oneOf([...RUN_QUALITIES, ''])])), }; + await waitForTableLength(page, 4); await validateTableData(page, new Map(Object.entries(tableDataValidatorsWithDetectorQualities))); await waitForNavigation(page, () => pressElement(page, '#synchronousFlags-tab')); + await page.waitForSelector('tbody tr:not(.loading-row)'); + const tableDataValidatorsWithQualityFromSynchronousFlags = { ...tableDataValidators, ...Object.fromEntries(DETECTORS.map((detectorName) => [ @@ -122,6 +125,7 @@ module.exports = () => { ])), }; + await waitForTableLength(page, 4); await validateTableData(page, new Map(Object.entries(tableDataValidatorsWithQualityFromSynchronousFlags))); await expectInnerText(page, '#row56-FT0', '83'); }); @@ -153,10 +157,16 @@ module.exports = () => { const amountSelectorButtonSelector = `${amountSelectorId} button`; await pressElement(page, amountSelectorButtonSelector); + await fillInput(page, `${amountSelectorId} input[type=number]`, '3', ['input', 'change']); + await waitForTableLength(page, 3); + await expectInnerText(page, '.dropup button', 'Rows per page: 3 '); + + await pressElement(page, amountSelectorButtonSelector); await page.waitForSelector(`${amountSelectorId} .dropup-menu`); const amountItems5 = `${amountSelectorId} .dropup-menu .menu-item:first-child`; await pressElement(page, amountItems5, true); + // only 4 runs in LHC Period 1 await waitForTableLength(page, 4); await expectInnerText(page, '.dropup button', 'Rows per page: 5 '); diff --git a/test/public/runs/runsPerSimulationPass.overview.test.js b/test/public/runs/runsPerSimulationPass.overview.test.js index 89f6d4703c..b7b1c725fd 100644 --- a/test/public/runs/runsPerSimulationPass.overview.test.js +++ b/test/public/runs/runsPerSimulationPass.overview.test.js @@ -166,6 +166,7 @@ module.exports = () => { const amountItems5 = `${amountSelectorId} .dropup-menu .menu-item:first-child`; await pressElement(page, amountItems5); + await page.waitForSelector(`${amountSelectorId} .dropup-menu`); await fillInput(page, `${amountSelectorId} input[type=number]`, 1111); await page.waitForSelector(amountSelectorId); });