From 444c41ca81c90ff172c7b1d99ff78f52bff991e8 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Tue, 12 Apr 2022 16:19:58 -0400 Subject: [PATCH] 16068: Fix flaky test maybe Arvados-DCO-1.1-Signed-off-by: Stephen Smith --- cypress/integration/collection.spec.js | 2 +- cypress/support/commands.js | 70 ++++++++++++++++++++++++++ cypress/support/index.d.ts | 24 +++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 cypress/support/index.d.ts diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js index 74acd056..79a66a4c 100644 --- a/cypress/integration/collection.spec.js +++ b/cypress/integration/collection.spec.js @@ -334,7 +334,7 @@ describe('Collection panel tests', function () { 'bar' // make sure we can go back to the original name as a last step ]; eachPair(names, (from, to) => { - cy.get('[data-cy=collection-files-panel]') + cy.waitForDom().get('[data-cy=collection-files-panel]') .contains(`${from}`).rightclick(); cy.get('[data-cy=context-menu]') .contains('Rename') diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5a2428b2..5764808c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -381,3 +381,73 @@ function b64toBlob(b64Data, contentType = '', sliceSize = 512) { const blob = new Blob(byteArrays, { type: contentType }); return blob } + +// From https://github.com/cypress-io/cypress/issues/7306#issuecomment-1076451070= +// This command requires the async package (https://www.npmjs.com/package/async) +Cypress.Commands.add('waitForDom', () => { + cy.window().then(win => { + let timeElapsed = 0; + + cy.log("Waiting for DOM mutations to complete"); + + return new Cypress.Promise((resolve) => { + // set the required variables + let async = require("async"); + let observerConfig = { attributes: true, childList: true, subtree: true }; + let items = Array.apply(null, { length: 50 }).map(Number.call, Number); + win.mutationCount = 0; + win.previousMutationCount = null; + + // create an observer instance + let observer = new win.MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Only record "attributes" type mutations that are not a "class" mutation. + // If the mutation is not an "attributes" type, then we always record it. + if (mutation.type === 'attributes' && mutation.attributeName !== 'class') { + win.mutationCount += 1; + } else if (mutation.type !== 'attributes') { + win.mutationCount += 1; + } + }); + + // initialize the previousMutationCount + if (win.previousMutationCount == null) win.previousMutationCount = 0; + }); + + // watch the document body for the specified mutations + observer.observe(win.document.body, observerConfig); + + // check the DOM for mutations up to 50 times for a maximum time of 5 seconds + async.eachSeries(items, function iteratee(item, callback) { + // keep track of the elapsed time so we can log it at the end of the command + timeElapsed = timeElapsed + 100; + + // make each iteration of the loop 100ms apart + setTimeout(() => { + if (win.mutationCount === win.previousMutationCount) { + // pass an argument to the async callback to exit the loop + return callback('Resolved - DOM changes complete.'); + } else if (win.previousMutationCount != null) { + // only set the previous count if the observer has checked the DOM at least once + win.previousMutationCount = win.mutationCount; + return callback(); + } else if (win.mutationCount === 0 && win.previousMutationCount == null && item === 4) { + // this is an early exit in case nothing is changing in the DOM. That way we only + // wait 500ms instead of the full 5 seconds when no DOM changes are occurring. + return callback('Resolved - Exiting early since no DOM changes were detected.'); + } else { + // proceed to the next iteration + return callback(); + } + }, 100); + }, function done() { + // Log the total wait time so users can see it + cy.log(`DOM mutations ${timeElapsed >= 5000 ? "did not complete" : "completed"} in ${timeElapsed} ms`); + + // disconnect the observer and resolve the promise + observer.disconnect(); + resolve(); + }); + }); + }); + }); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts new file mode 100644 index 00000000..d74d5b3d --- /dev/null +++ b/cypress/support/index.d.ts @@ -0,0 +1,24 @@ +/** + * This command tries to ensure that the elements in the DOM are actually visible + * and done (re)rendering. This is due to how React re-renders components. + * + * IMPORTANT NOTES: + * => You should only use this command in instances where a test is failing due + * to detached elements. Cypress will probably give you a warning along the lines + * of, "Element has an effective width/height of 0". This warning is not very useful + * in pointing out it is due to the element being detached from the DOM AFTER the + * cy.get command had already retrieved it. This command can save you from that + * by explicitly waiting for the DOM to stop changing. + * => This command can take anywhere from 100ms to 5 seconds to complete + * => This command will exit early (500ms) when no changes are occurring in the DOM. + * We wait a minimum of 500ms because sometimes it can take up to around that time + * for mutations to start occurring. + * + * GitHub Issues: + * * https://github.com/cypress-io/cypress/issues/695 (Closed - no activity) + * * https://github.com/cypress-io/cypress/issues/7306 (Open - re-get detached elements) + * + * @example Wait for the DOM to stop changing before retrieving an element + * cy.waitForDom().get('#an-elements-id') + */ + waitForDom(): Chainable -- 2.30.2