From 9856fd5180165d1862ac96d32b3c57c12f033879 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 17 Jul 2026 22:03:13 +0300 Subject: [PATCH] =?UTF-8?q?fix(web-test):=20=D0=B4=D0=B5=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D1=83-=D0=B4=D0=B5=D0=BA=D0=BE=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D0=BE=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9=20=D0=B2=D0=B2=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Страницы настроек (напр. «Администрирование → Интернет-поддержка и сервисы») собраны из гиперссылок, frameButton и сворачиваемых групп — без единого input.editInput / textarea / a.press. detectForm/detectForms считали такую форму отсутствующей → getFormState = {form:null, formCount:0}, навык её не видел. Расширен союзный селектор детекции (.staticTextHyper/.frameButton/ .checkbox/.radio/.tumblerItem/.grid). detectForm двухуровневый: обычные формы выбираются по редактируемым контролам (поведение не меняется), по расширенному счёту — только когда у формы нет ни одного поля ввода. form0 (рабочий стол) по-прежнему исключён фильтром n>0. Регресс: обработка-фикстура СтраницаНастроек (форма без командной панели: гиперссылка + сворачиваемая группа) в подсистеме Администрирование + тест 25-decoration-form. Полный набор 29 passed. Co-Authored-By: Claude Opus 4.8 --- .../skills/web-test/scripts/dom/_shared.mjs | 36 ++++++++++---- .../integration/build-webtest-config.test.mjs | 47 +++++++++++++++++++ tests/web-test/25-decoration-form.test.mjs | 36 ++++++++++++++ 3 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 tests/web-test/25-decoration-form.test.mjs diff --git a/.claude/skills/web-test/scripts/dom/_shared.mjs b/.claude/skills/web-test/scripts/dom/_shared.mjs index e9b161ce..7a50168b 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.4 — embedded JS function constants +// web-test dom shared v1.5 — embedded JS function constants // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Shared function strings embedded into page.evaluate() generators. @@ -308,34 +308,54 @@ function resolveColumnByName(model, name) { || null; }`; +// Селекторы детекции формы. EDIT_SEL — «редактируемые» контролы (поля/кнопки): их наличие +// исторически = «это форма». Но страницы настроек/справки (напр. «Интернет-поддержка и сервисы») +// собраны только из гиперссылок/frameButton/групп и НЕ имеют ни одного из этих трёх → форма не +// детектировалась (form=null). ANY_SEL добавляет контентные/интерактивные классы декораций, чтобы +// такие формы регистрировались. form0 (рабочий стол, тоже полон гиперссылок) исключается фильтром n>0. +const FORM_DETECT_EDIT_SEL = 'input.editInput[id], textarea[id], a.press[id]'; +const FORM_DETECT_ANY_SEL = FORM_DETECT_EDIT_SEL + ', .staticTextHyper[id], .frameButton[id], .checkbox[id], .radio[id], .tumblerItem[id], .grid[id]'; + /** Detect active form number. Picks form with most visible elements, skipping form0. * When modalSurface is visible — prefer the highest-numbered form (modal dialog). */ export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + ` function detectForm() { - const counts = {}; - document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => { + const editSel = ${JSON.stringify(FORM_DETECT_EDIT_SEL)}; + const anySel = ${JSON.stringify(FORM_DETECT_ANY_SEL)}; + const editCounts = {}; // строгие поля/кнопки + const anyCounts = {}; // + контентные декорации + document.querySelectorAll(anySel).forEach(el => { if (el.offsetWidth === 0) return; const m = el.id.match(/^form(\\d+)_/); - if (m) counts[m[1]] = (counts[m[1]] || 0) + 1; + if (!m) return; + anyCounts[m[1]] = (anyCounts[m[1]] || 0) + 1; + if (el.matches(editSel)) editCounts[m[1]] = (editCounts[m[1]] || 0) + 1; }); - const nums = Object.keys(counts).map(Number); + const nums = Object.keys(anyCounts).map(Number); if (!nums.length) return null; const candidates = nums.filter(n => n > 0); if (!candidates.length) return nums[0]; // When modal surface is visible, prefer the highest-numbered form (modal dialog) if (hasVisibleModal()) { const maxForm = Math.max(...candidates); - if (counts[maxForm] >= 1) return maxForm; + if (anyCounts[maxForm] >= 1) return maxForm; } - return candidates.reduce((best, n) => counts[n] > counts[best] ? n : best); + // Двухуровневый выбор: пока есть формы с редактируемыми контролами — выбираем по ним (прежнее + // поведение, обычные формы не сдвигаются). Только когда у ВСЕХ кандидатов их нет (info-страница) — + // выбираем по расширенному счёту. + const editable = candidates.filter(n => editCounts[n] > 0); + const pool = editable.length ? editable : candidates; + const metric = editable.length ? editCounts : anyCounts; + return pool.reduce((best, n) => metric[n] > metric[best] ? n : best); }`; /** Detect all open forms + modal state. Returns { activeForm, allForms, formCount, modal }. * Works even when the open-windows tab bar is hidden. */ export const DETECT_FORMS_FN = HAS_VISIBLE_MODAL_FN + ` function detectForms() { + const anySel = ${JSON.stringify(FORM_DETECT_ANY_SEL)}; const counts = {}; - document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => { + document.querySelectorAll(anySel).forEach(el => { if (el.offsetWidth === 0) return; const m = el.id.match(/^form(\\d+)_/); if (m) counts[m[1]] = (counts[m[1]] || 0) + 1; diff --git a/tests/skills/integration/build-webtest-config.test.mjs b/tests/skills/integration/build-webtest-config.test.mjs index c86fb6a2..eec944e7 100644 --- a/tests/skills/integration/build-webtest-config.test.mjs +++ b/tests/skills/integration/build-webtest-config.test.mjs @@ -1257,6 +1257,51 @@ export const steps = [ `, }, + // Обработка СтраницаНастроек — форма-«страница настроек» БЕЗ полей ввода: только + // гиперссылка-декорация + сворачиваемая группа (как «Администрирование → Интернет-поддержка + // и сервисы» в реальных конфигурациях). commandBarLocation:None убирает автокомандную панель, + // поэтому у формы НЕТ ни одного input.editInput / textarea / a.press — это воспроизводит баг + // детекции формы (detectForm возвращал null → getFormState = «No form detected»). Регресс на + // расширенный селектор детекции (dom/_shared.mjs). Открывается командой раздела и navigateLink. + { + name: 'meta-compile: Обработка СтраницаНастроек', + script: 'meta-compile/scripts/meta-compile', + input: { + type: 'DataProcessor', name: 'СтраницаНастроек', + }, + args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' }, + validate: { script: 'meta-validate/scripts/meta-validate', flag: '-ObjectPath', path: 'DataProcessors/СтраницаНастроек' }, + }, + { + name: 'form-add: Форма обработки СтраницаНастроек', + script: 'form-add/scripts/form-add', + args: { '-ObjectPath': '{workDir}/DataProcessors/СтраницаНастроек.xml', '-FormName': 'ФормаОбработки' }, + }, + { + name: 'form-compile: Форма обработки СтраницаНастроек', + script: 'form-compile/scripts/form-compile', + input: { + title: 'Страница настроек', + // Без командной панели → на форме нет ни одного a.press. + properties: { commandBarLocation: 'None' }, + attributes: [ + { name: 'Объект', type: 'DataProcessorObject.СтраницаНастроек', main: true }, + ], + elements: [ + // Гиперссылка-декорация → .staticTextHyper (кликабельна через findClickTargetScript). + { label: 'СсылкаТехническаяИнформация', title: 'Техническая информация', hyperlink: true }, + // Сворачиваемая группа с дочерней декорацией (пустая группа не рендерится). + { group: 'collapsible', name: 'ГруппаКлассификаторы', title: 'Классификаторы и курсы валют', + collapsed: true, showTitle: true, children: [ + { label: 'ПояснениеКлассификаторы', title: 'Загрузка курсов валют и классификаторов' }, + ], + }, + ], + }, + args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' }, + validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' }, + }, + // Обработка МножественныйВыбор — основная форма с 4 полями типа «список значений». { name: 'form-add: Основная форма МножественныйВыбор', @@ -1761,6 +1806,7 @@ export const steps = [ 'DataProcessor.ТестовыеОшибки', 'DataProcessor.ДеревоНоменклатуры', 'DataProcessor.ПроверкаДоступности', + 'DataProcessor.СтраницаНастроек', ], }, args: { '-DefinitionFile': '{inputFile}', '-OutputDir': '{workDir}' }, @@ -1786,6 +1832,7 @@ export const steps = [ 'DataProcessor.БезшапочнаяТаблица: Use View', 'DataProcessor.МногострочнаяШапка: Use View', 'DataProcessor.ПроверкаДоступности: Use View', + 'DataProcessor.СтраницаНастроек: Use View', ], }, args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' }, diff --git a/tests/web-test/25-decoration-form.test.mjs b/tests/web-test/25-decoration-form.test.mjs new file mode 100644 index 00000000..bee2e4f2 --- /dev/null +++ b/tests/web-test/25-decoration-form.test.mjs @@ -0,0 +1,36 @@ +export const name = 'decoration-form: форма без полей ввода (гиперссылки/группы) детектируется'; +export const tags = ['formstate', 'smoke']; +export const timeout = 90000; + +// Обработка СтраницаНастроек — форма-«страница настроек» БЕЗ единого input.editInput / +// textarea / a.press (commandBarLocation:None): только гиперссылка-декорация + сворачиваемая +// группа. Воспроизводит реальный кейс «Администрирование → Интернет-поддержка и сервисы». +// Регресс на расширенный селектор детекции формы (dom/_shared.mjs, DETECT_FORM_FN / +// DETECT_FORMS_FN): до фикса detectForm возвращал null → getFormState = «No form detected» +// (form:null, formCount:0), навык такую форму НЕ видел. + +export default async function({ navigateSection, openCommand, navigateLink, getFormState, closeForm, assert, step, log }) { + + await step('раздел: открыть «Страница настроек» командой из «Администрирование»', async () => { + await navigateSection('Администрирование'); + const r = await openCommand('Страница настроек'); + log(`form=${r.form} formCount=${r.formCount} activeTab=${r.activeTab}`); + assert.ok(r.form != null, 'форма распознана (form != null) — до фикса тут был null'); + assert.ok(r.formCount >= 1, 'formCount >= 1'); + const hy = (r.hyperlinks || []).map(h => h.name); + assert.includes(hy, 'Техническая информация', 'гиперссылка-декорация видна в состоянии'); + assert.equal((r.fields || []).length, 0, 'полей ввода на форме нет (декорации-only)'); + await closeForm(); + }); + + await step('navigateLink: та же форма через «Обработка.СтраницаНастроек»', async () => { + const r = await navigateLink('Обработка.СтраницаНастроек'); + log(`form=${r.form} formCount=${r.formCount} activeTab=${r.activeTab}`); + assert.ok(r.form != null, 'форма распознана через navigateLink'); + assert.ok(r.formCount >= 1, 'formCount >= 1'); + const state = await getFormState(); + const hy = (state.hyperlinks || []).map(h => h.name); + assert.includes(hy, 'Техническая информация', 'гиперссылка видна и в getFormState'); + await closeForm(); + }); +}