diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index b07c90a1..8324dc8d 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -217,6 +217,8 @@ Sections + all open tabs. ### Actions +**Return shape convention.** All action functions return a **flat form state** (same shape as `getFormState()`) with action-specific extras: `clicked`, `selected`, `filled`, `notFilled`, `closed`, `opened`, `navigated`, `deleted`, `filtered`, `unfiltered`. Errors always sit at the top level under `.errors` (when present) — the exec-wrapper automatically throws on `.errors.modal` / `.errors.balloon`. + #### `clickElement(text, { dblclick?, table?, expand?, modifier? })` → form state Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match). @@ -267,7 +269,7 @@ Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match). await clickElement('150 000', { dblclick: true }); // finds cell by text in report ``` -#### `fillFields({ name: value })` → `{ filled, form }` +#### `fillFields({ name: value })` → form state with `filled` Fill form fields by label (fuzzy match). Auto-detects field type. | Value | Field type | Method | @@ -286,7 +288,7 @@ await fillFields({ }); ``` -Returns `{ filled: [{ field, ok, value, method }], form: {...} }`. +Returns form state with `filled: [{ field, ok, value, method }]`. Method is one of: `'clear'` | `'toggle'` | `'radio'` | `'paste'` | `'dropdown'` | `'form'` | `'typeahead'` #### `selectValue(field, search, opts?)` → form state with `selected` @@ -310,9 +312,11 @@ await selectValue('Документ', '0000-000601', { type: 'Реализаци Also supports DCS labels — auto-enables the paired checkbox. -#### `fillTableRow(fields, opts)` → form state +#### `fillTableRow(fields, opts)` → form state with `filled` (+ optional `notFilled`) Fill table row cells via Tab navigation. Value is a plain string, `{ value, type }` for composite-type cells, or `''`/`null` to clear (Shift+F4). +Returns form state with `filled: [{ field, ok, method, value }]`. If some requested fields weren't reached (Tab loop couldn't find them), `notFilled: [...]` lists their names. + | Option | Description | |--------|-------------| | `tab` | Switch to tab before filling | diff --git a/.claude/skills/web-test/scripts/engine/forms/fill.mjs b/.claude/skills/web-test/scripts/engine/forms/fill.mjs index 1bb1deaf..bada6f2c 100644 --- a/.claude/skills/web-test/scripts/engine/forms/fill.mjs +++ b/.claude/skills/web-test/scripts/engine/forms/fill.mjs @@ -1,11 +1,11 @@ -// web-test forms/fill v1.17 — Fill form fields by name (text/checkbox/date/dropdown/reference). Delegates references to selectValue / fillReferenceField. +// web-test forms/fill v1.18 — Fill form fields by name (text/checkbox/date/dropdown/reference). Delegates references to selectValue / fillReferenceField. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { page, ensureConnected, ACTION_WAIT, highlightMode, normYo, } from '../core/state.mjs'; import { - detectFormScript, resolveFieldsScript, readFormScript, + detectFormScript, resolveFieldsScript, } from '../../dom.mjs'; import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs'; import { waitForStable, startNetworkMonitor } from '../core/wait.mjs'; @@ -13,9 +13,9 @@ import { highlight, unhighlight } from '../recording/highlight.mjs'; import { fillReferenceField, selectValue, pickFromSelectionForm, isTypeDialog, pickFromTypeDialog, -} from './select-value.mjs'; -import { pasteText } from '../core/clipboard.mjs'; -import { getFormState } from './state.mjs'; +} from './select-value.mjs'; +import { pasteText } from '../core/clipboard.mjs'; +import { returnFormState } from '../core/helpers.mjs'; /** Fill fields on the current form via Playwright page.fill(). Returns fill results + updated form. */ export async function fillFields(fields) { @@ -132,13 +132,12 @@ export async function fillFields(fields) { if (highlightMode) try { await unhighlight(); } catch {} } - const formData = await page.evaluate(readFormScript(formNum)); const failed = results.filter(r => r.error); if (failed.length > 0) { const details = failed.map(f => ` ${f.field}: ${f.message || f.error}${f.available ? ' (available: ' + f.available.join(', ') + ')' : ''}`).join('\n'); throw new Error(`fillFields: ${failed.length} of ${results.length} field(s) failed:\n${details}`); } - return { filled: results, form: formData }; + return returnFormState({ filled: results }); } /** Convenience alias: fill a single field. Same as fillFields({ name: value }). */ diff --git a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs index 262967d8..8f0ca11e 100644 --- a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs +++ b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs @@ -1,4 +1,4 @@ -// web-test table/row-fill v1.17 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений. +// web-test table/row-fill v1.18 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { @@ -18,7 +18,7 @@ import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs'; import { waitForStable, waitForCondition, startNetworkMonitor } from '../core/wait.mjs'; import { highlight, unhighlight } from '../recording/highlight.mjs'; import { - safeClick, findFieldInputId, + safeClick, findFieldInputId, returnFormState, detectNewForm as helperDetectNewForm, isInputFocused, isInputFocusedInGrid, findOpenPopup, readEdd, isEddVisible, clickEddItemViaDispatch, @@ -29,7 +29,6 @@ import { fillReferenceField, selectValue, } from '../forms/select-value.mjs'; import { pasteText } from '../core/clipboard.mjs'; -import { getFormState } from '../forms/state.mjs'; /** * Fill cells in the current table row via Tab navigation. @@ -112,7 +111,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { cellCoords.currentText.toLowerCase().includes(firstVal0.toLowerCase())) { firstFieldSkipped = true; if (Object.keys(fields).length === 1) { - return [{ field: firstKey0, ok: true, method: 'skip', value: cellCoords.currentText }]; + return returnFormState({ filled: [{ field: firstKey0, ok: true, method: 'skip', value: cellCoords.currentText }] }); } } @@ -146,11 +145,9 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { delete remaining[firstKey0]; if (Object.keys(remaining).length > 0) { const more = await fillTableRow(remaining, { row, table }); - if (Array.isArray(more)) results.push(...more); - else if (more?.filled) results.push(...more.filled); + results.push(...more.filled); } - const formData = await getFormState(); - return { filled: results, form: formData }; + return returnFormState({ filled: results }); } // Check if clicked cell is a checkbox (toggle-on-click, no edit mode) @@ -169,9 +166,9 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { delete remaining[firstKey0]; if (Object.keys(remaining).length > 0) { const more = await fillTableRow(remaining, { row, table }); - results.push(...more); + results.push(...more.filled); } - return results; + return returnFormState({ filled: results }); } let inEdit = false; @@ -338,7 +335,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { await page.keyboard.press('Escape'); } await waitForStable(formNum); - return results; + return returnFormState({ filled: results }); } if (!inEdit) throw new Error(`fillTableRow: click on row ${row} did not enter edit mode`); @@ -767,11 +764,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { ); if (currentRow >= 0) { const more = await fillTableRow(checkboxFields, { row: currentRow, table }); - if (Array.isArray(more)) { - results.push(...more); - } else if (more?.filled) { - results.push(...more.filled); - } + results.push(...more.filled); for (const key of Object.keys(checkboxFields)) { const idx = notFilled.indexOf(key); if (idx >= 0) notFilled.splice(idx, 1); @@ -780,11 +773,9 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } } - const formData = await getFormState(); - const result = { filled: results }; - if (notFilled.length > 0) result.notFilled = notFilled; - result.form = formData; - return result; + const extras = { filled: results }; + if (notFilled.length > 0) extras.notFilled = notFilled; + return returnFormState(extras); } catch (e) { if (e.message.startsWith('fillTableRow:')) throw e; diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index e0bd2433..2d6b71de 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -218,7 +218,7 @@ console.log('Расшифровка:', JSON.stringify(drilldown.rows)); | Функция | Описание | Возвращает | |---------|----------|------------| -| `navigateSection(name)` | Перейти в раздел (fuzzy match) | `{ sections, commands }` | +| `navigateSection(name)` | Перейти в раздел (fuzzy match) | form state с `navigated`, `sections`, `commands` | | `openCommand(name)` | Открыть команду из панели функций | form state | | `navigateLink(path)` | Открыть по пути метаданных (`Документ.ЗаказКлиента`) | form state | | `openFile(path)` | Открыть внешнюю обработку/отчёт (EPF/ERF) через «Файл → Открыть» | form state | @@ -286,12 +286,14 @@ await clickElement('150 000', { dblclick: true }); // найдёт ячейку ### Действия +Все action-функции возвращают **плоский form state** (как `getFormState()`) с action-specific extras (`clicked`, `selected`, `filled`, `notFilled`, `closed`, `opened`, `navigated`, `deleted`, `filtered`, `unfiltered`). Errors всегда на верхнем уровне `.errors` — exec-wrapper автоматически throw'ает на soft validation errors (`modal`/`balloon`). + | Функция | Описание | Возвращает | |---------|----------|------------| | `clickElement(text, {dblclick?, modifier?})` | Клик по кнопке/ссылке/строке. `{dblclick: true}` для открытия, `{modifier: 'ctrl'\|'shift'}` для мультиселекции. Первый аргумент может быть `{row, column}` для клика по ячейке SpreadsheetDocument (см. выше) | form state или `{ submenu }` | -| `fillFields({name: value})` | Заполнить поля (текст, чекбокс, радио, ссылки, DCS-фильтры). Пустое значение (`''`/`null`) = очистка | `{ filled: [{field, ok, method}], form }` | +| `fillFields({name: value})` | Заполнить поля (текст, чекбокс, радио, ссылки, DCS-фильтры). Пустое значение (`''`/`null`) = очистка | form state с `filled` | | `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст, `{поле: значение}` или `''`/`null` для очистки. `{ type }` для составного типа | form state с `selected` | -| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state | +| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state с `filled` (+ `notFilled?`) | | `deleteTableRow(row, {tab?})` | Удалить строку по индексу | form state | | `closeForm({save?})` | Закрыть форму. `save: false` = "Нет", `save: true` = "Да". Возвращает `closed: true/false` | form state с `closed` | | `filterList(text, {field?, exact?})` | Фильтр списка. Без field = все колонки, с field = расширенный поиск | form state | diff --git a/tests/web-test/05-table.test.mjs b/tests/web-test/05-table.test.mjs index 3285c5e7..8368cfa2 100644 --- a/tests/web-test/05-table.test.mjs +++ b/tests/web-test/05-table.test.mjs @@ -53,7 +53,7 @@ export default async function({ navigateSection, openCommand, clickElement, fill { 'Согласовано': true }, { table: 'Товары', row: 1 } ); - log(`checkbox result: ${JSON.stringify(r.filled || r)}`); + log(`checkbox result: ${JSON.stringify(r.filled)}`); const t = await readTable({ table: 'Товары' }); log(`row 1 Согласовано='${t.rows[1]['Согласовано']}'`); assert.equal(t.rows[1]['Согласовано'], 'true', 'Согласовано должно стать true'); @@ -65,7 +65,7 @@ export default async function({ navigateSection, openCommand, clickElement, fill { 'Номенклатура': '' }, { table: 'Товары', row: 0 } ); - log(`clear result: ${JSON.stringify(r.filled || r)}`); + log(`clear result: ${JSON.stringify(r.filled)}`); const t = await readTable({ table: 'Товары' }); log(`row 0 Номенклатура after clear='${t.rows[0]['Номенклатура']}'`); assert.equal(t.rows[0]['Номенклатура'], '', 'Номенклатура должна быть очищена (Shift+F4)');