mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 16:14:54 +03:00
refactor(web-test): uniform ok:true/false в filled-items + контракт fillTableRow (Phase 4)
Раньше per-item shape в filled[] был heterogeneous:
- success: {field, ok: true, method, value}
- failure: {field, error, message} ← без ok!
Естественная проверка `item.ok === false` молча промахивалась (latent bug в
production-клиенте Titan C:\WS\projects\titan\tests\helpers\query.mjs:69).
И документация утверждала «все функции throw'ают на ошибке» (guide.md:352),
что для fillTableRow было неправдой.
Что изменено:
- engine/table/row-fill.mjs v1.18 → v1.19: 15 error-pushes теперь включают
ok: false. item.ok — единый дискриминатор success/failure.
- SKILL.md: fillFields раздел уточнён («throws on per-field failure — если
вернулся, всё заполнено»). fillTableRow раздел: документирует контракт
(НЕ throws на per-field), перечисляет error-коды и recovery hints
(composite_type → retry с {value, type}, column_not_found → проверить
readTable, и т.д.).
- docs/web-test-guide.md: строка 352 нюансирована (fillTableRow исключение
из «все throw'ают»); строка 296 (таблица) уточнена.
Контракт обеих функций теперь сознательно различается и явно описан:
- fillFields = fail-fast (throws на любую ошибку, удобно для fill-and-go)
- fillTableRow = partial-recovery (errors в filled[] как ok:false, модель
может retry'нуть селективно отдельную ячейку)
Бонус: query.mjs в Titan'е теперь работает корректно без правки клиентского
кода — cell.ok === false наконец-то дискриминирует error-items.
Полный регресс 19/19 зелёный.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
|--------|-------------|
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)` перед чтением |
|
||||
|
||||
## Особенности
|
||||
|
||||
Reference in New Issue
Block a user