test(smoke): footer-width waits for the locale gate and its own dialog (#759)

smoke_dialog_footer_width switched the language by assigning
card._config and then measured the first hp-dialog in the tree. Since
#627 the main catalog for de/fr and the editor's settings/support/topology
dictionaries for ru/de/fr are lazy chunks; while one is in flight the
language gate keeps the previous frame (inert, aria-busy) and the dialog
is not rendered. The old wait only covered de and only the main catalog
(card._t('btn.save') === 'Speichern'), so under load the first dialog
after a switch (opening in ru/de) was read from the held frame and four
checks went red on a zero row.

Both page.evaluate blocks now wait by condition, like
smoke_dialog_polish_603 (#712): first for the gate's own markers (no
aria-busy, lang equals the requested language), then for
hp-dialog[data-kind=<kind>] to have its .dialog-action-footer laid out,
with a 5 s deadline and a named error. The measurement reads the dialog
of the requested kind instead of the first hp-dialog. Checks, names and
thresholds are unchanged (same 36 names under HP_SMOKE_CHECKS=1).

Runs on the branch: 10/10 sequential, 12/12 in 6 rounds of two parallel
copies (dev: 9/12 red under the same load); green with ru/de chunks
delayed 400 ms and 1500 ms and with only the editor dictionaries delayed
150 ms. Sabotage still bites: opening --hp-dialog-wide-width 560px reds
opening_*_medium_shell, physical footer buttons min-width 170px red
physical_*_three_actions_one_row and _positive_localization_headroom,
and an opening dialog that never renders fails with a named error.

Issue: #759
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 14:12:57 +03:00
parent 7ce3f6629f
commit b84465e541
+48 -14
View File
@@ -14,13 +14,30 @@ const desktop = await page.evaluate(async () => {
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
};
const settleLanguage = async (language) => {
for (let attempt = 0; language === 'de' && card._t('btn.save') !== 'Speichern'
&& attempt < 50; attempt++) {
await new Promise((resolve) => setTimeout(resolve, 10));
await settle();
// #759: a lazy locale keeps the previous frame, inert and aria-busy, until
// every dictionary this card paints has arrived — the main catalog and the
// editor's settings/support/topology namespaces (#627). The first dialog
// opened after a language switch is not in the tree until then. Wait for the
// gate's own markers, then for this dialog's footer to be laid out.
const waitFor = async (what, predicate) => {
const deadline = performance.now() + 5000;
for (;;) {
if (predicate()) return;
if (performance.now() > deadline) throw new Error(`smoke_dialog_footer_width: ${what}`);
await new Promise((resolve) => setTimeout(resolve, 16));
}
};
const settleLanguage = async (language, kind) => {
await waitFor(`язык ${language} не отрисован`,
() => !card.hasAttribute('aria-busy') && card.getAttribute('lang') === language);
await waitFor(`диалог ${kind} (${language}) не отрисован`, () => {
const root = card.shadowRoot || card.renderRoot;
const box = root.querySelector(`hp-dialog[data-kind="${kind}"]`)
?.querySelector('.dialog-action-footer')?.getBoundingClientRect();
return !!box && box.width > 0 && box.height > 0;
});
await settle();
};
card._serverStorage = true;
card._serverCfg = {
spaces: [{
@@ -37,7 +54,7 @@ const desktop = await page.evaluate(async () => {
const layout = (kind, language) => {
const root = card.shadowRoot || card.renderRoot;
const dialog = root.querySelector('hp-dialog');
const dialog = root.querySelector(`hp-dialog[data-kind="${kind}"]`);
const surface = dialog?.shadowRoot?.querySelector('.surface');
const footer = dialog?.querySelector('.dialog-action-footer');
const danger = footer?.querySelector('.dialog-action-danger');
@@ -87,7 +104,7 @@ const desktop = await page.evaluate(async () => {
card._openSpaceDialog('edit', 'dialog-layout');
}
await settle();
await settleLanguage(language);
await settleLanguage(language, kind);
const result = layout(kind, language);
card._openingDialog = null;
card._physicalDialog = null;
@@ -113,13 +130,30 @@ const narrow = await page.evaluate(async () => {
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
};
const settleLanguage = async (language) => {
for (let attempt = 0; language === 'de' && card._t('btn.save') !== 'Speichern'
&& attempt < 50; attempt++) {
await new Promise((resolve) => setTimeout(resolve, 10));
await settle();
// #759: a lazy locale keeps the previous frame, inert and aria-busy, until
// every dictionary this card paints has arrived — the main catalog and the
// editor's settings/support/topology namespaces (#627). The first dialog
// opened after a language switch is not in the tree until then. Wait for the
// gate's own markers, then for this dialog's footer to be laid out.
const waitFor = async (what, predicate) => {
const deadline = performance.now() + 5000;
for (;;) {
if (predicate()) return;
if (performance.now() > deadline) throw new Error(`smoke_dialog_footer_width: ${what}`);
await new Promise((resolve) => setTimeout(resolve, 16));
}
};
const settleLanguage = async (language, kind) => {
await waitFor(`язык ${language} не отрисован`,
() => !card.hasAttribute('aria-busy') && card.getAttribute('lang') === language);
await waitFor(`диалог ${kind} (${language}) не отрисован`, () => {
const root = card.shadowRoot || card.renderRoot;
const box = root.querySelector(`hp-dialog[data-kind="${kind}"]`)
?.querySelector('.dialog-action-footer')?.getBoundingClientRect();
return !!box && box.width > 0 && box.height > 0;
});
await settle();
};
const measure = async (kind, language = 'ru') => {
card._config = { ...(card._config || {}), language };
card._openingDialog = null;
@@ -139,9 +173,9 @@ const narrow = await page.evaluate(async () => {
card._openSpaceDialog('edit', 'dialog-layout');
}
await settle();
await settleLanguage(language);
await settleLanguage(language, kind);
const root = card.shadowRoot || card.renderRoot;
const dialog = root.querySelector('hp-dialog');
const dialog = root.querySelector(`hp-dialog[data-kind="${kind}"]`);
const surface = dialog?.shadowRoot?.querySelector('.surface');
const footer = dialog?.querySelector('.dialog-action-footer');
const danger = footer?.querySelector('.dialog-action-danger');