diff --git a/extensions/amp-iframe/1.0/component.js b/extensions/amp-iframe/1.0/component.js index 6b9a051e7478..b1d83c6419f4 100644 --- a/extensions/amp-iframe/1.0/component.js +++ b/extensions/amp-iframe/1.0/component.js @@ -72,9 +72,7 @@ export function BentoIframe({ useEffect(() => { const iframe = iframeRef.current; - // TODO(36239): Ensure that effects are properly isolated between test runs. - // Guarding for iframe truthiness should be enough. - if (!iframe?.ownerDocument.defaultView) { + if (!iframe) { return; } const win = getWin(iframe); diff --git a/extensions/amp-iframe/1.0/test/test-amp-iframe.js b/extensions/amp-iframe/1.0/test/test-amp-iframe.js index 5728b7ec0463..833c20a0d52c 100644 --- a/extensions/amp-iframe/1.0/test/test-amp-iframe.js +++ b/extensions/amp-iframe/1.0/test/test-amp-iframe.js @@ -3,6 +3,7 @@ import {htmlFor} from '#core/dom/static-template'; import {toggleExperiment} from '#experiments'; import {waitFor} from '#testing/test-helper'; import {whenUpgradedToCustomElement} from '#core/dom/amp-element-helpers'; +import {flush} from '#testing/preact'; describes.realWin( 'amp-iframe-v1.0', @@ -17,10 +18,8 @@ describes.realWin( async function waitRendered() { await whenUpgradedToCustomElement(element); await element.mount(); - await waitFor( - () => element.shadowRoot.querySelector('iframe'), - 'iframe rendered' - ); + await flush(); + await waitFor(() => element.shadowRoot.querySelector('iframe')); } beforeEach(() => { diff --git a/testing/init-tests.js b/testing/init-tests.js index 67eb039f5245..f176f27feaec 100644 --- a/testing/init-tests.js +++ b/testing/init-tests.js @@ -30,6 +30,7 @@ import { warnForConsoleError, } from './console-logging-setup'; import * as describes from './describes'; +import {flush as flushPreactEffects} from './preact'; import {TestConfig} from './test-config'; import {installYieldIt} from './yield'; @@ -156,8 +157,10 @@ function resetTestingState() { /** * Cleans up global state added during tests. + * @return {Promise} */ -function cleanupTestcase() { +async function cleanupTestcase() { + await flushPreactEffects(); setTestRunner(this); restoreConsoleSandbox(); restoreConsoleError(); diff --git a/testing/preact.js b/testing/preact.js new file mode 100644 index 000000000000..864930b83d14 --- /dev/null +++ b/testing/preact.js @@ -0,0 +1,64 @@ +import * as preact from /*OK*/ 'preact'; + +/** + * This file introduces a helper for draining Preact's queue of renders and effects. + * We use this as part of the afterEach() cleanup in unit tests, to ensure no effects are run + * in subsequent tests. + * + * There is still a test isolation issue in that an effect can asynchronously schedule work + * which cannot be guarded from at this layer. For that we'd likely need to refresh the window + * in between each test run. + * + * We should be able to remove this file if this feature lands in Preact. + * @fileoverview + */ + +const rafs = []; +/** + * @param {(ts: (DOMHighResTimeStamp) => number)} cb + * @return {number} + */ +function flushableRaf(cb) { + rafs.push(cb); + return requestAnimationFrame(flushRaf); +} + +function flushRaf(ts = performance.now()) { + for (const raf of rafs) { + raf(ts); + } + rafs.length = 0; +} + +let pendingRender; + +/** + * @param {() => void} process + * @return {Promise} + */ +async function flushableRender(process) { + pendingRender = () => { + pendingRender = null; + return process(); + }; + await Promise.resolve().then(pendingRender); +} + +/** + * Flushes Preact renders and effects. + * + * Effects may queue up further rerenders, etc. etc, + * so this function will loop until everything to resolves. + * + * @return {Promise} + */ +export async function flush() { + flushRaf(); + while (pendingRender) { + await pendingRender(); + flushRaf(); + } +} + +preact.options.requestAnimationFrame = flushableRaf; +preact.options.debounceRendering = flushableRender;