mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-14 18:04:58 +03:00
refactor(web-test): унификация shape fillFields + fillTableRow (Phase 3)
Все action-функции теперь возвращают плоский form state с extras —
закрыта последняя аномалия API. Раньше:
- fillFields → {filled, form} (вложенный, документировано в SKILL.md)
- fillTableRow → 3 разных shape в 5 ветках (array | {filled, form} | {filled, notFilled, form}),
при этом документация заявляла плоский — код её игнорировал
Теперь обе функции используют returnFormState({filled, notFilled?}) — тот же
паттерн что у всех action-функций после Phase 1+2 (clickElement, selectValue,
closeForm, filterList и т.д.).
Что закрывает:
1. Тихий баг в production-клиенте C:\WS\projects\titan\tests\helpers\query.mjs
на res.filled?.find() — array-ветки fillTableRow возвращали [{...}] без .filled
→ ошибки заполнения параметров запросов молча пропускались. R1/R2-аналог.
2. Костыли r.filled || r в tests/web-test/05-table.test.mjs (2 места) —
убраны, поскольку polymorphism устранён.
3. Расхождение код ↔ документация в fillTableRow.
4. Внутренний polymorphism в row-fill.mjs: убраны два `if (Array.isArray(more))`
костыля в рекурсивных вызовах самого fillTableRow.
Файлы:
- engine/forms/fill.mjs v1.17 → v1.18 (1 ветка → returnFormState)
- engine/table/row-fill.mjs v1.17 → v1.18 (5 веток + 2 рекурсии)
- tests/web-test/05-table.test.mjs (r.filled || r → r.filled)
- .claude/skills/web-test/SKILL.md (сигнатуры fillFields/fillTableRow + общая
ремарка про плоский return shape в начале раздела Actions)
- docs/web-test-guide.md (строки fillFields/fillTableRow/navigateSection;
общая ремарка в начале раздела «Действия»)
В тестах ни один кейс не обращался к .form.X, blast radius нулевой.
Точечный регресс (03/05/06/07/10/16) и полный регресс 19/19 — зелёные.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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';
|
||||
} 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 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 returnFormState({ filled: results });
|
||||
}
|
||||
|
||||
/** Convenience alias: fill a single field. Same as fillFields({ name: value }). */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)');
|
||||
|
||||
Reference in New Issue
Block a user