From f037324ee936cba1c0b4f9cace00188281dcb406 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 21 Mar 2026 17:23:31 +0300 Subject: [PATCH] feat(web-test): expose formCount, openForms, modal in getFormState; closed in closeForm When the open-windows tab bar is hidden in 1C settings, the model had no way to know how many forms are open or whether a form is modal. Now getFormState returns openForms/formCount/modal derived from DOM form elements (independent of tab bar), and closeForm compares form number before/after Escape to return closed: true/false. Tested on ncc (tab bar hidden) and bpdemo (tab bar visible). Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/web-test/SKILL.md | 18 ++++++++++--- .claude/skills/web-test/scripts/browser.mjs | 13 +++++++--- .claude/skills/web-test/scripts/dom.mjs | 28 ++++++++++++++++++--- docs/web-test-guide.md | 9 +++++-- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index d089c512..5b298bfb 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -121,9 +121,19 @@ Switch to an already-open tab/window (fuzzy match). ### Reading form state -#### `getFormState()` → `{ fields, buttons, tabs, navigation?, table, tables, filters, reportSettings? }` +#### `getFormState()` → `{ form, formCount, openForms, fields, buttons, tabs, navigation?, table, tables, filters, reportSettings? }` Returns current form structure. This is the primary way to understand what's on screen. +**form** — active form number, or `null` when no form is open (desktop). + +**formCount** — number of open forms. Use this to know how many windows are stacked. `0` means desktop. + +**openForms** — array of all open form numbers (e.g. `[0, 1]`). Works even when the open-windows tab bar is hidden in 1C settings. + +**modal** — `true` when the active form is a modal dialog blocking the UI. Only present when modal is active. + +**openTabs** — array of `{ name, active? }` from the open-windows tab bar. Only present when the tab bar is enabled in 1C settings. Do NOT rely on this — use `formCount`/`openForms` instead. + **fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields) **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. "Основное", "Объекты метаданных", "Подсистемы"). @@ -303,8 +313,8 @@ await fillTableRow( #### `deleteTableRow(row, { tab?, table? })` → form state Delete row by 0-based index. `table` targets a specific grid on multi-grid forms. -#### `closeForm({ save? })` → form state -Close the current form via Escape. +#### `closeForm({ save? })` → form state with `closed` +Close the current form via Escape. Returns form state with `closed: true/false` indicating whether the form actually closed. | Argument | Behavior | |----------|----------| @@ -312,6 +322,8 @@ Close the current form via Escape. | `{ save: true }` | Auto-clicks "Да" on confirmation | | `{}` (omitted) | Returns `confirmation` field if dialog appears | +**`closed`** — `true` if the form was closed (form number changed), `false` if it stayed open (e.g. Escape was ignored). Always check this to confirm the form actually closed. After closing, check `formCount` to see how many forms remain. + Preferred over `clickElement('×')` — close buttons on tabs are ambiguous. #### `filterList(text, opts?)` → form state diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index cdc40431..2742e905 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -1,4 +1,4 @@ -// web-test browser v1.4 — Playwright browser management for 1C web client +// web-test browser v1.5 — Playwright browser management for 1C web client // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Playwright browser management for 1C web client. @@ -1893,8 +1893,9 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = { export async function closeForm({ save } = {}) { ensureConnected(); await dismissPendingErrors(); + const beforeForm = await page.evaluate(detectFormScript()); await page.keyboard.press('Escape'); - await waitForStable(); + await waitForStable(beforeForm); const state = await getFormState(); const err = await checkForErrors(); if (err?.confirmation) { @@ -1906,15 +1907,19 @@ export async function closeForm({ save } = {}) { const txt = (await b.textContent()).trim(); if (txt === label) { await b.click({ force: true }); - await waitForStable(); + await waitForStable(beforeForm); break; } } - return await getFormState(); + const afterState = await getFormState(); + afterState.closed = afterState.form !== beforeForm; + return afterState; } state.confirmation = err.confirmation; state.hint = 'Confirmation dialog shown. Click "Да" to confirm or "Нет" to cancel'; + return state; } + state.closed = state.form !== beforeForm; return state; } diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index 854cbf53..3544dc66 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -1,4 +1,4 @@ -// web-test dom v1.1 — DOM selectors and semantic mapping for 1C web client +// web-test dom v1.2 — DOM selectors and semantic mapping for 1C web client // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * DOM selectors and semantic mapping for 1C:Enterprise web client. @@ -32,6 +32,21 @@ const DETECT_FORM_FN = `function detectForm() { return candidates.reduce((best, n) => counts[n] > counts[best] ? n : best); }`; +/** Detect all open forms + modal state. Returns { activeForm, allForms, formCount, modal }. + * Works even when the open-windows tab bar is hidden. */ +const DETECT_FORMS_FN = `function detectForms() { + const counts = {}; + document.querySelectorAll('input.editInput[id], a.press[id]').forEach(el => { + if (el.offsetWidth === 0) return; + const m = el.id.match(/^form(\\d+)_/); + if (m) counts[m[1]] = (counts[m[1]] || 0) + 1; + }); + const nums = Object.keys(counts).map(Number); + const modal = document.getElementById('modalSurface'); + const isModal = !!(modal && modal.offsetWidth > 0); + return { allForms: nums.sort((a, b) => a - b), formCount: nums.length, modal: isModal }; +}`; + /** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */ const READ_FORM_FN = `function readForm(p) { const result = {}; @@ -586,12 +601,14 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto export function getFormStateScript() { return `(() => { ${DETECT_FORM_FN} + ${DETECT_FORMS_FN} ${READ_FORM_FN} const formNum = detectForm(); - if (formNum === null) return { form: null, message: 'No form detected' }; + const meta = detectForms(); + if (formNum === null) return { form: null, formCount: 0, message: 'No form detected' }; const p = 'form' + formNum + '_'; const formData = readForm(p); - // Open tabs bar + // Open tabs bar (present only when tab panel is enabled in 1C settings) const openTabs = []; document.querySelectorAll('[id^="openedCell_cmd_"]').forEach(el => { const text = el.innerText?.trim(); @@ -601,7 +618,10 @@ export function getFormStateScript() { openTabs.push(entry); }); const activeTab = openTabs.find(t => t.active)?.name || null; - return { form: formNum, activeTab, ...formData }; + const result = { form: formNum, activeTab, openForms: meta.allForms, formCount: meta.formCount, ...formData }; + if (meta.modal) result.modal = true; + if (openTabs.length) result.openTabs = openTabs; + return result; })()`; } diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index b81b4833..4aa0a09b 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -205,7 +205,7 @@ await closeForm({ save: false }); | Функция | Описание | Возвращает | |---------|----------|------------| -| `getFormState()` | Структура формы: поля, кнопки, таблица, фильтры | `{ fields, buttons, tabs, table, filters, reportSettings? }` | +| `getFormState()` | Структура формы: поля, кнопки, таблица, фильтры, состояние окон | `{ form, formCount, openForms, fields, buttons, tabs, table, filters, reportSettings? }` | | `readTable({ maxRows?, offset? })` | Данные таблицы с пагинацией | `{ columns, rows: [{col: val}], total }` | | `readSpreadsheet()` | Результат отчёта | `{ title?, headers?, data?, totals?, total }` | | `getSections()` | Разделы и команды | `{ activeSection, sections, commands }` | @@ -215,6 +215,11 @@ await closeForm({ save: false }); Основной способ «увидеть» что на экране: +- **form** — номер активной формы, `null` когда ничего не открыто (десктоп) +- **formCount** — количество открытых форм. `0` = десктоп. Работает даже если панель открытых окон скрыта +- **openForms** — `[0, 1, 2]` — номера всех открытых форм в DOM +- **modal** — `true` когда активная форма — модальный диалог, блокирующий интерфейс +- **openTabs** — `[{ name, active? }]` из панели открытых окон (только когда панель включена в настройках 1С) - **fields** — `[{ name, value, label?, actions?, required? }]`. `actions` = select/clear/open. `required: true` = незаполненное обязательное поле - **table** — `{ name, columns, rowCount }` (метаданные; для данных — `readTable()`) - **reportSettings** — DCS-фильтры в читаемом виде: `[{ name: "Склад", enabled: true, value: "..." }]` @@ -239,7 +244,7 @@ await closeForm({ save: false }); | `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст или `{поле: значение}`. `{ type }` для составного типа | form state с `selected` | | `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка или `{ value, type }` для составного типа | form state | | `deleteTableRow(row, {tab?})` | Удалить строку по индексу | form state | -| `closeForm({save?})` | Закрыть форму. `save: false` = "Нет", `save: true` = "Да" | form state | +| `closeForm({save?})` | Закрыть форму. `save: false` = "Нет", `save: true` = "Да". Возвращает `closed: true/false` | form state с `closed` | | `filterList(text, {field?, exact?})` | Фильтр списка. Без field = все колонки, с field = расширенный поиск | form state | | `unfilterList({field?})` | Снять фильтры (все или конкретный) | form state |