From b84465e541b2e3391c88357dd94c159f34ee162c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 14:12:57 +0300 Subject: [PATCH] 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=] 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 Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd --- demo/smoke_dialog_footer_width.mjs | 62 +++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/demo/smoke_dialog_footer_width.mjs b/demo/smoke_dialog_footer_width.mjs index c5b555cf..67724abc 100644 --- a/demo/smoke_dialog_footer_width.mjs +++ b/demo/smoke_dialog_footer_width.mjs @@ -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');