diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 8324dc8d..5d34f789 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -288,8 +288,7 @@ await fillFields({ }); ``` -Returns form state with `filled: [{ field, ok, value, method }]`. -Method is one of: `'clear'` | `'toggle'` | `'radio'` | `'paste'` | `'dropdown'` | `'form'` | `'typeahead'` +Returns form state with `filled: [{ field, ok: true, value, method }]` (method: `clear`|`toggle`|`radio`|`paste`|`dropdown`|`form`|`typeahead`). **Throws on any per-field failure** with a detailed message listing problematic fields and available options — if the call returned, all fields were filled, no per-item check needed. #### `selectValue(field, search, opts?)` → form state with `selected` Select a value from reference field via dropdown or selection form. More reliable than `fillFields` for reference fields that need exact selection from a catalog. Pass empty `search` (`''` or `null`) to clear the field (Shift+F4). @@ -315,7 +314,9 @@ Also supports DCS labels — auto-enables the paired checkbox. #### `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. +Returns form state with `filled: [{ field, ok, ...}]`. Items are `{ field, ok: true, method, value }` on success (method: `direct`|`paste`|`dropdown`|`form`|`type-direct`|`skip`|`clear`|`toggle`) or `{ field, ok: false, error, message }` on per-field failure. Unmatched fields → `notFilled: [...]`. + +**Unlike `fillFields`, `fillTableRow` does NOT throw on per-field failures** — errors appear as `ok: false` items in `filled[]` so the caller can react selectively (e.g. retry one cell while the rest of the row stays filled). Check via `r.filled.filter(f => !f.ok)`. Error codes: `composite_type`/`type_required`/`type_dialog_failed` (retry with `{value, type}`); `column_not_found` (check column name via `readTable`); `no_selection_form`/`no_selection_after_type` (retry or fall back to `selectValue`); `not_found`/`no_match`/`ambiguous` (refine search text); `still_open` (picked a group — pick a leaf row). Soft validation errors from 1C (`balloon`, `modal`) still throw via the exec-wrapper. | Option | Description | |--------|-------------| 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 8f0ca11e..3ce7ed78 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.18 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений. +// web-test table/row-fill v1.19 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { @@ -242,17 +242,17 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { // After type selection, detect the actual selection form selForm = await helperDetectNewForm(formNum); if (selForm === null) { - return { field: key, error: 'no_selection_after_type', message: `Type selected but no selection form opened for "${key}"` }; + return { field: key, ok: false, error: 'no_selection_after_type', message: `Type selected but no selection form opened for "${key}"` }; } } else { // No type specified — close type dialog and report error await page.keyboard.press('Escape'); await page.waitForTimeout(300); - return { field: key, error: 'composite_type', message: `Composite type field "${key}" requires {value, type}` }; + return { field: key, ok: false, error: 'composite_type', message: `Composite type field "${key}" requires {value, type}` }; } } const pr = await pickFromSelectionForm(selForm, key, info.value, formNum); - return pr.ok ? { field: key, ok: true, method: 'form' } : { field: key, error: pr.error, message: pr.message }; + return pr.ok ? { field: key, ok: true, method: 'form' } : { field: key, ok: false, error: pr.error, message: pr.message }; } // First field: selection form is already open from the dblclick above @@ -277,7 +277,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { const nextCoords = await page.evaluate(findNextCellCoordsByKeyScript(gridSelector, row, key)); if (!nextCoords) { info.filled = true; - results.push({ field: key, error: 'column_not_found', message: `Column for "${key}" not found` }); + results.push({ field: key, ok: false, error: 'column_not_found', message: `Column for "${key}" not found` }); continue; } // Skip if cell already contains the desired value @@ -319,7 +319,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } if (selForm === null) { info.filled = true; - results.push({ field: key, error: 'no_selection_form', message: `Dblclick on "${key}" did not open selection form` }); + results.push({ field: key, ok: false, error: 'no_selection_form', message: `Dblclick on "${key}" did not open selection form` }); continue; } const pr = await directEditPick(selForm, key, info); @@ -511,7 +511,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { info.filled = true; results.push(pickResult.ok ? { field: matchedKey, cell: cell.fullName, ok: true, method: 'form', type: info.type } - : { field: matchedKey, cell: cell.fullName, + : { field: matchedKey, cell: cell.fullName, ok: false, error: pickResult.error, message: pickResult.message }); continue; } @@ -521,7 +521,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { await page.waitForTimeout(300); } info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, + results.push({ field: matchedKey, cell: cell.fullName, ok: false, error: 'type_dialog_failed', message: `Cell "${matchedKey}": F4 did not open type dialog for type "${info.type}"` }); await page.keyboard.press('Tab'); @@ -539,7 +539,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { if (!inputAfterPaste && text) { // No type specified — can't fill this composite-type cell info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, + results.push({ field: matchedKey, cell: cell.fullName, ok: false, error: 'type_required', message: `Cell "${matchedKey}" rejected text input (composite-type). Use { value: '...', type: 'Тип' } syntax` }); await page.keyboard.press('Tab'); @@ -575,7 +575,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, + results.push({ field: matchedKey, cell: cell.fullName, ok: false, error: 'not_found', message: `No match for "${text}"` }); } @@ -611,16 +611,16 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { continue; } // Not found in selection form — fall through to clear + skip - results.push({ field: matchedKey, cell: cell.fullName, + results.push({ field: matchedKey, cell: cell.fullName, ok: false, error: pickResult.error, message: pickResult.message }); } else { info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, + results.push({ field: matchedKey, cell: cell.fullName, ok: false, error: 'not_found', message: `Value "${text}" not in list` }); } } else { info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, + results.push({ field: matchedKey, cell: cell.fullName, ok: false, error: 'not_found', message: `Value "${text}" not in list` }); } @@ -686,7 +686,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { info.filled = true; results.push(pickResult.ok ? { field: matchedKey, cell: cell.fullName, ok: true, method: 'form', type: info.type } - : { field: matchedKey, cell: cell.fullName, + : { field: matchedKey, cell: cell.fullName, ok: false, error: pickResult.error, message: pickResult.message }); continue; } else { @@ -699,7 +699,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { await page.keyboard.press('Tab'); await page.waitForTimeout(500); info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, + results.push({ field: matchedKey, cell: cell.fullName, ok: false, error: 'type_required', message: `Cell "${matchedKey}" opened a type selection dialog. Use { value: '...', type: 'Тип' } syntax` }); continue; @@ -710,7 +710,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { info.filled = true; results.push(pickResult.ok ? { field: matchedKey, cell: cell.fullName, ok: true, method: 'form' } - : { field: matchedKey, cell: cell.fullName, + : { field: matchedKey, cell: cell.fullName, ok: false, error: pickResult.error, message: pickResult.message }); continue; } diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index 2d6b71de..74cb3bf9 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -293,7 +293,7 @@ await clickElement('150 000', { dblclick: true }); // найдёт ячейку | `clickElement(text, {dblclick?, modifier?})` | Клик по кнопке/ссылке/строке. `{dblclick: true}` для открытия, `{modifier: 'ctrl'\|'shift'}` для мультиселекции. Первый аргумент может быть `{row, column}` для клика по ячейке SpreadsheetDocument (см. выше) | form state или `{ submenu }` | | `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 с `filled` (+ `notFilled?`) | +| `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state с `filled` (per-field ошибки как items `ok: false`, см. ниже) + `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 | @@ -349,13 +349,16 @@ await page.keyboard.press('F8'); // пример: создать новый э ## Типичные ошибки -Все функции бросают исключение при ошибке (не возвращают `{ error }`). Сценарий прерывается на проблемном шаге с информативным сообщением. В интерактиве — `try/catch` для обработки. +Большинство функций бросают исключение при ошибке. Сценарий прерывается на проблемном шаге с информативным сообщением. В интерактиве — `try/catch` для обработки. + +**Исключение — `fillTableRow`**: на per-field ошибках не throws, а возвращает их в `filled[]` как items с `ok: false` (`{ field, ok: false, error: 'code', message: '...' }`). Это позволяет частичное восстановление: например при `error: 'composite_type'` модель может retry'нуть конкретную ячейку с `{ value, type }` синтаксисом, не перезаполняя всю строку. Проверка — `r.filled.filter(f => !f.ok)`. Жёсткие ошибки (нет формы, table не найдена) и soft validation errors от 1С (balloon/modal) — всё равно throws. | Проблема | Решение | |----------|---------| | `no form found` — форма не открыта | Добавьте `await wait(2)` после навигации | | `not found. Available: ...` — элемент не найден | Проверьте имя через `getFormState()`, используйте вариант из Available | | `fillFields: N of M field(s) failed` | Текст ошибки содержит список проблемных полей и доступные варианты | +| `fillTableRow` вернул item с `ok: false` | См. поле `error` — `composite_type` → retry с `{value, type}`; `column_not_found` → проверьте имя поля через `readTable`; `not_found` → уточните значение поиска | | Пустой `readSpreadsheet()` | Увеличьте `await wait(N)` перед чтением | ## Особенности