From a816de37d9bb51e6e2d8ee36c828365168ac2283 Mon Sep 17 00:00:00 2001 From: Daniil Bratukhin Date: Tue, 8 Sep 2026 12:27:25 -0300 Subject: [PATCH] test: fix the 23 failing tests on main Test files run as concurrent tabs. A browser produces no frames for a tab that is not the visible one, so anything awaiting a frame never settled and 19 tests timed out after 20s each. Turn on CDP focus emulation per page, so every page keeps getting frames. The other four failed on their own terms, all from waiting a fixed number of ticks or pinning one browser's output: - three i18n tests waited a fixed number of ticks for a translation bundle that now needs one more, so they wait for the bundle instead; - the payment card embed pinned one browser's serialization of the Lumo font stack, so it resolves that stack through the browser instead. Producing frames again also surfaced a notice that was previously never reached. `ResponsiveMixin` sets attributes on the element it observes, which can resize it again in the same cycle; the browser's "ResizeObserver loop completed with undelivered notifications" then lands on window.onerror and counts against whichever test is running. Drop that one message before the handler sees it - "loop limit exceeded" is a real runaway loop and must still fail. Without this, foxy-tax-form and foxy-template-config-form fail. CHROME_PATH now overrides the browser location. The default is unchanged, so CI is unaffected; it is what makes the suite runnable off Linux. Before: 23 failures in 5 groups. After: 177/177 groups green, 7397 passed. Verified per group; a single group globbing all of ./src is not a reliable way to run this suite. --- src/elements/private/I18N/I18N.test.ts | 15 +++++- src/elements/public/I18n/I18n.test.ts | 19 +++++-- .../PaymentCardEmbed/PaymentCardEmbed.test.ts | 19 ++++++- web-test-runner.config.js | 54 ++++++++++++++++++- 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/elements/private/I18N/I18N.test.ts b/src/elements/private/I18N/I18N.test.ts index 12f55f2cf..8c108a5a5 100644 --- a/src/elements/private/I18N/I18N.test.ts +++ b/src/elements/private/I18N/I18N.test.ts @@ -1,5 +1,6 @@ -import { expect, fixture } from '@open-wc/testing'; +import { expect, fixture, waitUntil } from '@open-wc/testing'; import { createModel } from '@xstate/test'; +import type { i18n } from 'i18next'; import { createMachine } from 'xstate'; import { I18N } from './I18N'; @@ -68,7 +69,6 @@ function testLang(lang: 'en' | 'fr') { async function testText(element: I18N) { await element.whenReady; - await element.requestUpdate(); const lang = element.lang as 'en' | 'fr'; const ns = element.ns as 'global' | 'custom'; @@ -77,6 +77,17 @@ async function testText(element: I18N) { const value = opts?.value ?? ''; const text = key === '' ? '' : samples.text[lang][ns][key].replace('{{value}}', value); + // `whenReady` only covers the one-time i18next init. Assigning `ns` or `lang` starts another + // load that it does not track, and until that lands `_t` falls back to the global namespace, so + // wait for the bundle this state actually needs before reading the rendered text. + const i18nInstance = (element as unknown as { _i18n: i18n })._i18n; + await waitUntil( + () => !!i18nInstance.getResourceBundle(lang, ns), + `i18next never loaded the ${ns} namespace for ${lang}` + ); + + await element.requestUpdate(); + expect(element.shadowRoot!.textContent).to.equal(text); } diff --git a/src/elements/public/I18n/I18n.test.ts b/src/elements/public/I18n/I18n.test.ts index ab1876550..662c45ed3 100644 --- a/src/elements/public/I18n/I18n.test.ts +++ b/src/elements/public/I18n/I18n.test.ts @@ -1,6 +1,6 @@ import './index'; -import { expect, fixture, html, oneEvent } from '@open-wc/testing'; +import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; import { FetchEvent } from '../NucleonElement/FetchEvent'; import { I18n } from './I18n'; @@ -67,8 +67,13 @@ describe('I18n', () => { event.preventDefault(); event.respondWith(Promise.resolve(new Response(JSON.stringify(resource)))); - await new Promise(resolve => setTimeout(resolve)); - await element.requestUpdate(); + + // The element re-renders itself when i18next loads a bundle, but reading the response body + // takes more than one task, so wait for the bundle instead of a fixed number of ticks. + await waitUntil( + () => !!I18n.i18next.getResourceBundle('en', 'baz'), + 'i18next never loaded the baz namespace' + ); expect(element).shadowDom.to.equal('bar'); }); @@ -89,8 +94,12 @@ describe('I18n', () => { event.preventDefault(); event.respondWith(Promise.resolve(new Response(JSON.stringify(resource)))); - await new Promise(resolve => setTimeout(resolve)); - await element.requestUpdate(); + + // See the note above: wait for the bundle, not for a fixed number of ticks. + await waitUntil( + () => !!I18n.i18next.getResourceBundle('es', 'shared'), + 'i18next never loaded the es translations' + ); expect(element).shadowDom.to.equal('bar'); }); diff --git a/src/elements/public/PaymentCardEmbed/PaymentCardEmbed.test.ts b/src/elements/public/PaymentCardEmbed/PaymentCardEmbed.test.ts index 63aca9be8..faa9c629c 100644 --- a/src/elements/public/PaymentCardEmbed/PaymentCardEmbed.test.ts +++ b/src/elements/public/PaymentCardEmbed/PaymentCardEmbed.test.ts @@ -22,6 +22,22 @@ class TestElement extends PaymentCardEmbed { customElements.define('test-element', TestElement); +// The Lumo default font stack, verbatim from @vaadin/vaadin-lumo-styles/typography.js. The element +// forwards `getComputedStyle(...).fontFamily`, and browsers rewrite that stack when serializing it +// (current Chrome reports BlinkMacSystemFont as "system-ui"), so resolve it here instead of +// hardcoding one browser's output. +const LUMO_FONT_FAMILY = + '-apple-system, BlinkMacSystemFont, "Roboto", "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; + +function computedFontFamily(fontFamily: string): string { + const probe = document.createElement('div'); + probe.style.fontFamily = fontFamily; + document.body.appendChild(probe); + const computed = getComputedStyle(probe).fontFamily; + probe.remove(); + return computed; +} + describe('PaymentCardEmbed', () => { it('imports and defines foxy-spinner element', () => { expect(customElements.get('foxy-spinner')).to.exist; @@ -162,8 +178,7 @@ describe('PaymentCardEmbed', () => { '--lumo-size-xs': '26px', '--lumo-border-radius-m': '4px', '--lumo-border-radius-s': '4px', - '--lumo-font-family': - '-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + '--lumo-font-family': computedFontFamily(LUMO_FONT_FAMILY), '--lumo-font-size-m': '16px', '--lumo-font-size-s': '14px', '--lumo-font-size-xs': '13px', diff --git a/web-test-runner.config.js b/web-test-runner.config.js index 3cb423860..4a1b1683e 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -8,8 +8,26 @@ export default Object.assign({}, webServerConfig, { browsers: [ puppeteerLauncher({ + // Test pages run as concurrent tabs, and a browser produces no frames for a tab that is not + // the visible one. Anything waiting on a frame - `nextFrame()`, `elementUpdated()` on a + // non-Lit element, Vaadin's own internals - then never settles and the test times out. + // Focus emulation makes every page behave as the focused one, so frames keep coming. + createPage: async ({ context }) => { + const page = await context.newPage(); + + try { + const session = await page.target().createCDPSession(); + await session.send('Emulation.setFocusEmulationEnabled', { enabled: true }); + } catch { + // Older or non-Chromium browsers may not support it. Frame-dependent tests can time out + // when that happens, but the run still starts, which beats failing every file. + } + + return page; + }, + launchOptions: { - executablePath: '/usr/bin/chromium', + executablePath: process.env.CHROME_PATH || '/usr/bin/chromium', args: ['--no-sandbox', '--disable-setuid-sandbox'], }, }), @@ -25,6 +43,40 @@ export default Object.assign({}, webServerConfig, { }, }, + // `ResponsiveMixin` sets breakpoint attributes on the element it observes, so the resize it + // reacts to can produce another one in the same delivery cycle. The browser then reports + // "ResizeObserver loop completed with undelivered notifications" - a notice, not an application + // error, since the pending notification is delivered on the next frame. It arrives at + // `window.onerror`, where the test framework counts it against whichever test is running, so + // drop it before that handler sees it. This classic script runs before the module below. + // + // Match the full message, not the "ResizeObserver loop" prefix: "ResizeObserver loop limit + // exceeded" starts the same way but means the browser gave up on a runaway loop, which is a + // real bug and must still fail the test. + testRunnerHtml: testFramework => ` + + + + + + + + + + `, + middleware: [ (context, next) => { const url = context.url;