Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/elements/private/I18N/I18N.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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';
Expand All @@ -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);
}

Expand Down
19 changes: 14 additions & 5 deletions src/elements/public/I18n/I18n.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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('<span>bar</span>');
});
Expand All @@ -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('<span>bar</span>');
});
Expand Down
19 changes: 17 additions & 2 deletions src/elements/public/PaymentCardEmbed/PaymentCardEmbed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
54 changes: 53 additions & 1 deletion web-test-runner.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
}),
Expand All @@ -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 => `
<!DOCTYPE html>
<html>
<head>
<script>
window.addEventListener(
'error',
event => {
var benign = 'ResizeObserver loop completed with undelivered notifications';
if (event.message && event.message.indexOf(benign) === 0) {
event.stopImmediatePropagation();
event.preventDefault();
}
},
true
);
</script>
</head>
<body>
<script type="module" src="${testFramework}"></script>
</body>
</html>
`,

middleware: [
(context, next) => {
const url = context.url;
Expand Down