From fc2323f3216c0188483f679ca8d0052806b1cd88 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 18 Jul 2026 13:18:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20=D0=B2=D1=81=D0=BF=D0=BB?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=8E=D1=89=D0=B8=D0=B5=20(popup)=20=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=BF=D0=BF=D1=8B=20=E2=80=94=20behavior,=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5,=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Popup-группа надёжно отличается от сворачиваемой по DOM-маркеру панели #panel_div (+ #CloseBtn). В getFormState().groups она помечается behavior:'popup', а её collapsed берётся из display панели (закрыта = collapsed:true), не из инлайн-сиблинга (у popup содержимое в отдельном слое, а не под mainGroup). clickElement по заголовку popup и открывает, и закрывает — тот же словарь {expand}/{toggle}, что и у сворачиваемых. После открытия содержимое панели становится читаемым в getFormState (fields/ hyperlinks/texts). Тест 25-decoration-form покрывает popup; SKILL дополнен. Полный регресс 29 passed. Co-Authored-By: Claude Opus 4.8 --- .claude/skills/web-test/SKILL.md | 2 +- .../skills/web-test/scripts/dom/_shared.mjs | 36 +++++++++++-------- .claude/skills/web-test/scripts/dom/forms.mjs | 19 +++++----- tests/web-test/25-decoration-form.test.mjs | 18 ++++++++++ 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 3310012e..fd07007f 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -146,7 +146,7 @@ Returns current form structure. This is the primary way to understand what's on **navigation** — form navigation panel links (for objects with subordinate catalogs): `[{ name, active? }]`. Clickable via `clickElement()`. Only present when the form has a navigation panel (e.g. "Основное", "Объекты метаданных", "Подсистемы"). -**groups** — collapsible form groups: `[{ name, title, collapsed }]`. `collapsed: true` means the group's content is hidden — part of the form is not shown until you expand it (common on settings pages like "Администрирование → Интернет-поддержка и сервисы"). Expand/collapse a group by its title with `clickElement`, same vocabulary as tree nodes: `{ expand: true }` opens (idempotent), `{ expand: false }` collapses, `{ toggle: true }` flips. Plain (non-collapsible) groups are not listed. +**groups** — collapsible and pop-up form groups: `[{ name, title, collapsed, behavior? }]`. `collapsed: true` means the group's content is hidden — part of the form is not shown until you expand it (common on settings pages like "Администрирование → Интернет-поддержка и сервисы"). `behavior: 'popup'` marks a pop-up group (content shows in a floating panel); absent for ordinary collapsible groups. Expand/collapse (or open/close a pop-up) by the group title with `clickElement`, same vocabulary as tree nodes: `{ expand: true }` reveals (idempotent), `{ expand: false }` hides, `{ toggle: true }` flips. After expanding, the group's content becomes readable in the next `getFormState()` (its fields/hyperlinks/texts appear). Plain (non-collapsible) groups are not listed. ```js const form = await getFormState(); // form.groups = [{ name: "ГруппаНовости", title: "Новости", collapsed: true }, ...] diff --git a/.claude/skills/web-test/scripts/dom/_shared.mjs b/.claude/skills/web-test/scripts/dom/_shared.mjs index 730049e5..3cc49528 100644 --- a/.claude/skills/web-test/scripts/dom/_shared.mjs +++ b/.claude/skills/web-test/scripts/dom/_shared.mjs @@ -1,4 +1,4 @@ -// web-test dom shared v1.6 — embedded JS function constants +// web-test dom shared v1.7 — embedded JS function constants // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Shared function strings embedded into page.evaluate() generators. @@ -675,26 +675,32 @@ function readForm(p) { if (iframeCount) result.iframes = iframeCount; // Collapsible / popup groups — surface that part of the form is hidden + its state. - // Идентификация раскрываемой группы: у неё есть заголовок #title_text, и либо этот - // заголовок — гиперссылка (.staticTextHyper: ControlRepresentation=TitleHyperlink / popup), - // либо рядом есть кнопка-каретка #titleBtn (ControlRepresentation=Picture). Обычные - // (несворачиваемые) группы не имеют ни того, ни другого — их не показываем. - // Состояние: DOM у 1С плоский (контент группы — сиблинги под mainGroup, не вложены), но при - // глубинном обходе Form.xml ПЕРВЫЙ контент-сиблинг сразу за #title_div — всегда дочерний - // элемент группы (свободные соседи идут после всех детей). Его display = состояние группы: - // none → свёрнута, block → развёрнута. Переживает свободные элементы между группами. + // Идентификация раскрываемой группы (есть заголовок #title_text и один из признаков): + // • заголовок — гиперссылка (.staticTextHyper: ControlRepresentation=TitleHyperlink); + // • рядом кнопка-каретка #titleBtn (ControlRepresentation=Picture); + // • есть панель #panel_div — это ВСПЛЫВАЮЩАЯ (popup) группа. + // Обычные (несворачиваемые) группы не имеют ничего из этого — их не показываем. + // Состояние: + // • popup: display панели #panel_div (none → закрыта); + // • collapsible: DOM у 1С плоский (контент — сиблинги под mainGroup, не вложены), но при + // глубинном обходе Form.xml ПЕРВЫЙ контент-сиблинг сразу за #title_div — всегда дочерний + // элемент группы (свободные соседи идут после всех детей). Его display = состояние. const groups = []; document.querySelectorAll('[id^="' + p + '"][id$="#title_text"]').forEach(tt => { if (tt.offsetWidth === 0 && tt.offsetHeight === 0) return; const base = tt.id.slice(0, -('#title_text'.length)); + const panelDiv = document.getElementById(base + '#panel_div'); // popup-маркер const isHyper = tt.classList.contains('staticTextHyper'); const hasBtn = !!document.getElementById(base + '#titleBtn'); - if (!isHyper && !hasBtn) return; // обычная (несворачиваемая) группа — пропускаем - const title = nbsp(tt.innerText?.trim() || ''); - const g = { name: base.replace(p, ''), title }; - const titleDiv = document.getElementById(base + '#title_div'); - const contentSib = titleDiv?.nextElementSibling; - if (contentSib) g.collapsed = getComputedStyle(contentSib).display === 'none'; + if (!isHyper && !hasBtn && !panelDiv) return; // обычная (несворачиваемая) группа + const g = { name: base.replace(p, ''), title: nbsp(tt.innerText?.trim() || '') }; + if (panelDiv) { + g.behavior = 'popup'; + g.collapsed = getComputedStyle(panelDiv).display === 'none'; + } else { + const contentSib = document.getElementById(base + '#title_div')?.nextElementSibling; + if (contentSib) g.collapsed = getComputedStyle(contentSib).display === 'none'; + } groups.push(g); }); diff --git a/.claude/skills/web-test/scripts/dom/forms.mjs b/.claude/skills/web-test/scripts/dom/forms.mjs index de2fb446..e587b902 100644 --- a/.claude/skills/web-test/scripts/dom/forms.mjs +++ b/.claude/skills/web-test/scripts/dom/forms.mjs @@ -1,4 +1,4 @@ -// web-test dom/forms v1.11 — form detection, content read, click-target/field-button resolution +// web-test dom/forms v1.12 — form detection, content read, click-target/field-button resolution // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { DETECT_FORM_FN, READ_FORM_FN, ROW_CLICK_POINT_FN } from './_shared.mjs'; @@ -105,23 +105,24 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector } }); // Сворачиваемые/всплывающие группы — заголовок как цель раскрытия/сворачивания. - // Идентификация: #title_text — гиперссылка (TitleHyperlink/popup) ЛИБО рядом есть - // кнопка-каретка #titleBtn (ControlRepresentation=Picture). Обычные группы пропускаем. - // Мишень клика: #titleBtn (вариант «картинка») иначе сам заголовок-гиперссылка. - // Состояние: первый контент-сиблинг за #title_div (display:none = свёрнута). + // Идентификация: #title_text и один из: гиперссылка (TitleHyperlink) ЛИБО кнопка-каретка + // #titleBtn (Picture) ЛИБО панель #panel_div (popup). Обычные группы пропускаем. + // Мишень клика: #titleBtn (вариант «картинка») иначе заголовок (у popup клик по заголовку + // и открывает, и закрывает). Состояние: у popup — display панели, иначе — первый контент- + // сиблинг за #title_div (display:none = свёрнута/закрыта). [...document.querySelectorAll('[id^="' + p + '"][id$="#title_text"]')] .filter(el => el.offsetWidth > 0 || el.offsetHeight > 0).forEach(el => { const base = el.id.slice(0, -('#title_text'.length)); const btn = document.getElementById(base + '#titleBtn'); const btnVisible = btn && (btn.offsetWidth > 0 || btn.offsetHeight > 0); - if (!el.classList.contains('staticTextHyper') && !btnVisible) return; // обычная группа + const panelDiv = document.getElementById(base + '#panel_div'); + if (!el.classList.contains('staticTextHyper') && !btnVisible && !panelDiv) return; // обычная группа const tgt = btnVisible ? btn : el; const r = tgt.getBoundingClientRect(); - const titleDiv = document.getElementById(base + '#title_div'); - const sib = titleDiv && titleDiv.nextElementSibling; const item = { id: '', kind: 'formGroup', name: norm(el.innerText) || base.replace(p, ''), label: base.replace(p, ''), x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }; - if (sib) item.collapsed = getComputedStyle(sib).display === 'none'; + const stateEl = panelDiv || (document.getElementById(base + '#title_div') || {}).nextElementSibling; + if (stateEl) item.collapsed = getComputedStyle(stateEl).display === 'none'; items.push(item); }); diff --git a/tests/web-test/25-decoration-form.test.mjs b/tests/web-test/25-decoration-form.test.mjs index 746a9677..bc3515c7 100644 --- a/tests/web-test/25-decoration-form.test.mjs +++ b/tests/web-test/25-decoration-form.test.mjs @@ -70,4 +70,22 @@ export default async function({ navigateSection, openCommand, navigateLink, getF assert.equal(collapsedOf(r, 'ГруппаКартинкой'), false, 'B раскрыта тоглом'); await closeForm(); }); + + await step('popup: всплывающая группа — behavior, открытие показывает содержимое', async () => { + const s = await navigateLink('Обработка.СтраницаНастроек'); + const pg = (s.groups || []).find(g => g.name === 'ГруппаВсплывающая'); + assert.ok(pg, 'всплывающая группа в groups[]'); + assert.equal(pg.behavior, 'popup', 'помечена behavior:popup (отличима от collapsible)'); + assert.equal(pg.collapsed, true, 'закрыта — collapsed:true'); + assert.ok(!(s.texts || []).some(t => /всплывающей/.test(t.value)), 'содержимое скрыто, пока закрыта'); + // Открыть → содержимое панели становится видно в состоянии формы. + let r = await clickElement('Всплывающая группа', { expand: true }); + assert.equal((r.groups || []).find(g => g.name === 'ГруппаВсплывающая')?.collapsed, false, 'открыта'); + assert.ok((r.texts || []).some(t => /всплывающей/.test(t.value)), 'после открытия содержимое видно'); + r = await clickElement('Всплывающая группа', { expand: true }); + assert.equal(r.clicked.toggled, false, 'expand идемпотентен для popup'); + r = await clickElement('Всплывающая группа', { expand: false }); + assert.equal((r.groups || []).find(g => g.name === 'ГруппаВсплывающая')?.collapsed, true, 'закрыта обратно'); + await closeForm(); + }); }