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) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-21 17:23:31 +03:00
parent 6f36e36166
commit f037324ee9
4 changed files with 55 additions and 13 deletions
+15 -3
View File
@@ -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
+9 -4
View File
@@ -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;
}
+24 -4
View File
@@ -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;
})()`;
}
+7 -2
View File
@@ -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 |