mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-06 03:08:57 +03:00
feat(web-test): fillFields/одиночный selectValue на полях-списках значений + фиксы
Модель может заполнять поля-списки одним значением или массивом через fillFields/
selectValue — раньше это виснет (DLB→форма «Выбор значений», safeClick по surface)
или throw «type dialog» (CB→«Список значений», ложный isTypeDialog). Статически
поле-список не отличить от обычного рефа → детекция поведенческая.
- Рефактор-шов: selectValuesMulti = открытие + dispatchMultiSurface(field,values,
{formNum,isCloudDD,baseForm}); детекторы detectMultiSurface/isValueListSurface.
- Делегация на УЖЕ открытой форме (без переоткрытия/мигания), асимметрично по пути:
DLB (fillReferenceField) — по «DLB открыл форму»; CB (одиночный selectValue, до
isTypeDialog) — по Подбор/СнятьФлажки. Обычный реф (DLB→инлайн / форма «Выбрать»
без этих кнопок) НЕ делегируется.
- fillFields: массив → selectValue(array); одиночное течёт в существующие
hasSelect/hasPick ветки (фикс внутри них).
- Фикс clearPool для платформенного пула: Ctrl+A не выделяет все строки → цикл
построчного удаления (кастомный пул с чекбоксами — через СнятьФлажки).
Регресс 25/25 (обычный реф Контрагент НЕ делегирован). Доп. кейсы в
22-multiselect-listfield. select-value v1.33, fill v1.20.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -348,6 +348,8 @@ actually selected; any value the field doesn't offer goes to `notSelected: [{ va
|
||||
await selectValue('Наименование компании', ['Альфа ООО', 'Бета АО']);
|
||||
// selected: { field: 'Наименование компании', values: ['Альфа ООО', 'Бета АО'] }
|
||||
```
|
||||
A **single** value works too — `selectValue('Поле', 'Альфа ООО')` or `fillFields({ 'Поле': 'Альфа ООО' })`
|
||||
on a value-list field is auto-routed to the same mechanism (no need to know the field is a value-list).
|
||||
|
||||
#### `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).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test forms/fill v1.19 — Fill form fields by name (text/checkbox/date/number/dropdown/reference). Delegates references to selectValue / fillReferenceField.
|
||||
// web-test forms/fill v1.20 — Fill form fields by name (text/checkbox/date/number/dropdown/reference; array → multi-select via selectValue). Delegates references to selectValue / fillReferenceField.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -33,6 +33,20 @@ export async function fillFields(fields) {
|
||||
results.push(r);
|
||||
continue;
|
||||
}
|
||||
// Array value → multi-select. Delegate to selectValue's array branch (auto-detects
|
||||
// the surface) so fillFields({field:[...]}) works the same as selectValue(field,[...]).
|
||||
if (Array.isArray(fields[r.field])) {
|
||||
const sv = await selectValue(r.field, fields[r.field]);
|
||||
const sel = sv.selected || {};
|
||||
if (sel.error) {
|
||||
results.push({ field: r.field, error: sel.error });
|
||||
} else {
|
||||
const res = { field: r.field, ok: true, values: sel.values || [], method: 'multi-select' };
|
||||
if (sel.notSelected?.length) res.notSelected = sel.notSelected;
|
||||
results.push(res);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Auto-highlight the field input before filling
|
||||
if (highlightMode && r.inputId) {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test forms/select-value v1.30 — Reference & composite-type value selection: selectValue (+ array multi-select), fillReferenceField, selection/type-dialog pickers.
|
||||
// web-test forms/select-value v1.33 — Reference & composite-type value selection: selectValue (+ array multi-select), fillReferenceField, selection/type-dialog pickers.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -453,6 +453,16 @@ export async function fillReferenceField(selector, fieldName, value, formNum) {
|
||||
if (dlbVisible) {
|
||||
await page.click(dlbSelector);
|
||||
await page.waitForTimeout(1000);
|
||||
// Value-list field: DLB opened a FORM (a normal reference's DLB opens an inline dropdown,
|
||||
// not a form) → delegate to the multi handler on the already-open form (single value =
|
||||
// 1-element set). This avoids hanging on the safeClick below over the modal .surface.
|
||||
const openedForm = await detectNewForm();
|
||||
if (openedForm !== null) {
|
||||
const res = await dispatchMultiSurface(fieldName, [text], { formNum: openedForm, baseForm: formNum });
|
||||
const sel = res.selected || {};
|
||||
return { field: fieldName, ok: !sel.error, value: text, method: 'multi-select',
|
||||
...(sel.notSelected ? { notSelected: sel.notSelected } : {}) };
|
||||
}
|
||||
const eddState = await readEdd();
|
||||
if (eddState.visible && eddState.items?.length > 0) {
|
||||
const target = normYo(text.toLowerCase());
|
||||
@@ -659,18 +669,12 @@ async function selectValuesMulti(fieldName, values, { type } = {}) {
|
||||
const baseForm = await page.evaluate(detectFormScript());
|
||||
if (baseForm === null) throw new Error('selectValue(multi): no form found');
|
||||
|
||||
const valStr = (el) => typeof el === 'string' ? el : String(Object.values(el)[0]);
|
||||
const targets = values.map(v => ({ raw: v, str: valStr(v) }));
|
||||
const selected = [], notSelected = [];
|
||||
const eqYo = (a, b) => normYo(String(a).toLowerCase()) === normYo(String(b).toLowerCase());
|
||||
const incYo = (hay, needle) => normYo(String(hay).toLowerCase()).includes(normYo(String(needle).toLowerCase()));
|
||||
|
||||
// Resolve field button (DLB→CB) + DCS-checkbox auto-enable (mirror selectValue).
|
||||
let btn = await page.evaluate(findFieldButtonScript(baseForm, fieldName, 'DLB'));
|
||||
if (btn?.error === 'button_not_found') btn = await page.evaluate(findFieldButtonScript(baseForm, fieldName, 'CB'));
|
||||
if (btn?.error) {
|
||||
return returnFormState({ selected: { field: fieldName, values: [], error: 'field_not_found',
|
||||
notSelected: targets.map(t => ({ value: t.str, reason: 'field_not_found' })) } });
|
||||
return returnFormState({ selected: { field: fieldName, values: [],
|
||||
error: 'field_not_found', notSelected: values.map(v => ({ value: String(v), reason: 'field_not_found' })) } });
|
||||
}
|
||||
if (btn.dcsCheckbox) {
|
||||
const cbSel = `[id="${btn.dcsCheckbox.inputId}"]`;
|
||||
@@ -679,9 +683,7 @@ async function selectValuesMulti(fieldName, values, { type } = {}) {
|
||||
if (!isChecked) { await page.click(cbSel).catch(() => {}); await waitForStable(); }
|
||||
}
|
||||
|
||||
const cloudDDVisible = () => page.evaluate(`!![...document.querySelectorAll('.cloudDD')].find(p => p.offsetWidth > 0 && p.offsetHeight > 0)`);
|
||||
|
||||
// ── Open + detect (F4-first) ──
|
||||
// ── Open (F4-first) ──
|
||||
// clickElement already stabilizes the page internally (and leaves the field focused) — so we
|
||||
// detect a new form right away, and on the F4 branch press F4 directly with NO re-focus click.
|
||||
// (A page.click on the field input here can intermittently hang ~30s on Playwright's
|
||||
@@ -694,22 +696,58 @@ async function selectValuesMulti(fieldName, values, { type } = {}) {
|
||||
await waitForStable(baseForm);
|
||||
formNum = await helperDetectNewForm(baseForm);
|
||||
}
|
||||
|
||||
let surface = null;
|
||||
if (formNum !== null) {
|
||||
const fs = await getFormState();
|
||||
const buttons = fs.buttons || [];
|
||||
const hasButton = (label) => buttons.some(b => (b.name || '').includes(label) || (b.tooltip || '').includes(label));
|
||||
if (hasButton(MULTI_BTN.podbor)) surface = MULTI_SURFACE.poolPodbor;
|
||||
else if (hasButton(MULTI_BTN.choose) && !hasButton(MULTI_BTN.confirm)) surface = MULTI_SURFACE.catalogMultiRow;
|
||||
else if (hasButton(MULTI_BTN.uncheckAll) || hasButton(MULTI_BTN.confirm)) surface = MULTI_SURFACE.checkboxForm;
|
||||
} else if (await cloudDDVisible()) {
|
||||
surface = MULTI_SURFACE.cloudDropdown;
|
||||
const isCloudDD = (formNum === null) && await cloudDDVisible();
|
||||
if (formNum === null && !isCloudDD) {
|
||||
return returnFormState({ selected: { field: fieldName, values: [],
|
||||
error: 'surface_unrecognized', notSelected: values.map(v => ({ value: String(v), reason: 'open_failed' })) } });
|
||||
}
|
||||
return dispatchMultiSurface(fieldName, values, { formNum, isCloudDD, baseForm });
|
||||
}
|
||||
|
||||
const cloudDDVisible = () => page.evaluate(`!![...document.querySelectorAll('.cloudDD')].find(p => p.offsetWidth > 0 && p.offsetHeight > 0)`);
|
||||
|
||||
/** Classify an already-open form as a value-list multi-surface (or null). For our own dispatch —
|
||||
* here the field is already known to be a value-list, so the broad Выбрать/ОК signals are safe. */
|
||||
async function detectMultiSurface(formNum) {
|
||||
if (formNum === null) return null;
|
||||
const fs = await getFormState();
|
||||
const buttons = fs.buttons || [];
|
||||
const hasButton = (label) => buttons.some(b => (b.name || '').includes(label) || (b.tooltip || '').includes(label));
|
||||
if (hasButton(MULTI_BTN.podbor)) return MULTI_SURFACE.poolPodbor;
|
||||
if (hasButton(MULTI_BTN.choose) && !hasButton(MULTI_BTN.confirm)) return MULTI_SURFACE.catalogMultiRow;
|
||||
if (hasButton(MULTI_BTN.uncheckAll) || hasButton(MULTI_BTN.confirm)) return MULTI_SURFACE.checkboxForm;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** CONSERVATIVE check used by the single-value selectValue delegation: only the UNAMBIGUOUS
|
||||
* value-list signals (Подбор / СнятьФлажки) — a normal single-reference selection form has
|
||||
* "Выбрать" (no ОК/Подбор/СнятьФлажки), so it is NOT misclassified as a value-list surface. */
|
||||
async function isValueListSurface(formNum) {
|
||||
if (formNum === null) return false;
|
||||
const fs = await getFormState();
|
||||
const buttons = fs.buttons || [];
|
||||
const hasButton = (label) => buttons.some(b => (b.name || '').includes(label) || (b.tooltip || '').includes(label));
|
||||
return hasButton(MULTI_BTN.podbor) || hasButton(MULTI_BTN.uncheckAll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the multi-select for a value-list field whose selection surface is ALREADY OPEN
|
||||
* (`formNum` form, or `isCloudDD` inline dropdown). Detects which of the 4 surfaces it is
|
||||
* and applies `values` (always replace). Shared by selectValuesMulti (after it opens) and by
|
||||
* the single-value delegation in selectValue / fillReferenceField (reuses the open form — no
|
||||
* reopen, no flicker). Returns flat state + `selected:{field,values,notSelected?}`.
|
||||
*/
|
||||
async function dispatchMultiSurface(fieldName, values, { formNum, isCloudDD, baseForm }) {
|
||||
const valStr = (el) => typeof el === 'string' ? el : String(Object.values(el)[0]);
|
||||
const targets = values.map(v => ({ raw: v, str: valStr(v) }));
|
||||
const selected = [], notSelected = [];
|
||||
const eqYo = (a, b) => normYo(String(a).toLowerCase()) === normYo(String(b).toLowerCase());
|
||||
const incYo = (hay, needle) => normYo(String(hay).toLowerCase()).includes(normYo(String(needle).toLowerCase()));
|
||||
|
||||
const surface = isCloudDD ? MULTI_SURFACE.cloudDropdown : await detectMultiSurface(formNum);
|
||||
if (!surface) {
|
||||
return returnFormState({ selected: { field: fieldName, values: [], error: 'surface_unrecognized',
|
||||
notSelected: targets.map(t => ({ value: t.str, reason: 'open_failed' })) } });
|
||||
return returnFormState({ selected: { field: fieldName, values: [],
|
||||
error: 'surface_unrecognized', notSelected: targets.map(t => ({ value: t.str, reason: 'open_failed' })) } });
|
||||
}
|
||||
|
||||
// ── Per-surface handlers ──
|
||||
@@ -797,15 +835,29 @@ async function selectValuesMulti(fieldName, values, { type } = {}) {
|
||||
|
||||
// Empty the pool by selecting every row (Ctrl+A covers off-screen rows) and deleting.
|
||||
async function clearPool() {
|
||||
const t0 = await readTable({ maxRows: 200 });
|
||||
if (!(t0.rows || []).length) return;
|
||||
try { await clickElement({ row: 0, column: 'Колонка1' }); } catch {} // focus grid (make list active)
|
||||
let t = await readTable({ maxRows: 200 });
|
||||
if (!(t.rows || []).length) return;
|
||||
// Custom pool: rows are checkboxes (membership = checked) → just uncheck all (rows persist).
|
||||
if ((t.columns || []).includes('(checkbox)')) {
|
||||
await clickElement(MULTI_BTN.uncheckAll).catch(() => {});
|
||||
return;
|
||||
}
|
||||
// Platform pool (plain rows, membership = presence) → delete every row. Ctrl+A does NOT
|
||||
// select all here (only the focused row), so: one select-all+Delete, then delete the first
|
||||
// row repeatedly until the pool is empty (guard against an unexpected non-shrinking grid).
|
||||
const colKey = t.columns?.[0] || 'Колонка1';
|
||||
try { await clickElement({ row: 0, column: colKey }); } catch {}
|
||||
await page.keyboard.press('Control+a');
|
||||
await page.waitForTimeout(200);
|
||||
await page.waitForTimeout(150);
|
||||
await page.keyboard.press('Delete');
|
||||
await waitForStable(formNum);
|
||||
const t1 = await readTable({ maxRows: 50 });
|
||||
if ((t1.rows || []).length) await clickElement(MULTI_BTN.uncheckAll).catch(() => {}); // fallback
|
||||
for (let guard = 0; guard < 100; guard++) {
|
||||
t = await readTable({ maxRows: 200 });
|
||||
if (!(t.rows || []).length) return;
|
||||
try { await clickElement({ row: 0, column: colKey }); } catch {}
|
||||
await page.keyboard.press('Delete');
|
||||
await waitForStable(formNum);
|
||||
}
|
||||
}
|
||||
|
||||
// Surface: inline .cloudDD quick-choice dropdown. Coordinate clicks (a .surface
|
||||
@@ -987,6 +1039,10 @@ export async function selectValue(fieldName, searchText, { type } = {}) {
|
||||
await waitForStable(formNum);
|
||||
const selFormNum = await detectSelectionForm();
|
||||
if (selFormNum !== null) {
|
||||
// Value-list field — delegate to the multi handler on the already-open form.
|
||||
if (await isValueListSurface(selFormNum)) {
|
||||
return dispatchMultiSurface(btn.fieldName, [searchText], { formNum: selFormNum, baseForm: formNum });
|
||||
}
|
||||
const pickResult = await pickFromSelectionForm(selFormNum, btn.fieldName, searchText || '', formNum);
|
||||
const selected = { field: btn.fieldName, search: searchText || null, method: 'form' };
|
||||
if (pickResult.error) selected.error = pickResult.error;
|
||||
@@ -1081,6 +1137,12 @@ export async function selectValue(fieldName, searchText, { type } = {}) {
|
||||
// 3B. Check if a new selection form opened directly (use broad detection to also catch type dialogs)
|
||||
const selFormNum = await detectNewForm();
|
||||
if (selFormNum !== null) {
|
||||
// Value-list field (Подбор/СнятьФлажки surface) — delegate to the multi handler on the
|
||||
// already-open form (a single value = a 1-element set). MUST precede isTypeDialog: the platform
|
||||
// "Список значений" form has a ValueList grid + ОК and would be misclassified as a type dialog.
|
||||
if (await isValueListSurface(selFormNum)) {
|
||||
return dispatchMultiSurface(btn.fieldName, [searchText], { formNum: selFormNum, baseForm: formNum });
|
||||
}
|
||||
// Auto-detect type selection dialog when `type` was not specified
|
||||
if (await isTypeDialog(selFormNum)) {
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
@@ -9,7 +9,7 @@ export const timeout = 420000;
|
||||
|
||||
const sortq = a => [...a].sort();
|
||||
|
||||
export default async function({ navigateLink, selectValue, getFormState, wait, assert, step, log }) {
|
||||
export default async function({ navigateLink, selectValue, fillFields, getFormState, wait, assert, step, log }) {
|
||||
|
||||
const fieldValue = async (name) => {
|
||||
const fs = await getFormState();
|
||||
@@ -83,4 +83,33 @@ export default async function({ navigateLink, selectValue, getFormState, wait, a
|
||||
assert.deepEqual(sortq(r.selected.values), sortq(['ООО Север', 'ООО Юг']), 'оба контрагента подобраны');
|
||||
assert.ok(!r.selected.notSelected, 'notSelected отсутствует');
|
||||
});
|
||||
|
||||
// ── Делегирование через fillFields / одиночный selectValue (поведенческая детекция) ──
|
||||
await step('fillFields одиночное на поле-списке (DLB) → делегирует, не виснет', async () => {
|
||||
const f = await fillFields({ 'Через флажки': 'Альфа' });
|
||||
log(`fillFields single: ${JSON.stringify(f.filled)}`);
|
||||
assert.ok(f.filled?.[0]?.ok, 'значение проставлено без зависания');
|
||||
assert.equal(await fieldValue('ЧерезФлажки'), 'Альфа', 'поле = Альфа');
|
||||
});
|
||||
|
||||
await step('fillFields массивом на поле-списке → делегирует в мультивыбор', async () => {
|
||||
const f = await fillFields({ 'Через флажки': ['Альфа', 'Бета'] });
|
||||
log(`fillFields array: ${JSON.stringify(f.filled)}`);
|
||||
assert.deepEqual(sortq(f.filled?.[0]?.values || []), sortq(['Альфа', 'Бета']), 'оба выбраны');
|
||||
assert.equal(await fieldValue('ЧерезФлажки'), 'Альфа, Бета', 'поле = набор');
|
||||
});
|
||||
|
||||
await step('fillFields одиночное на платформенном (CB) → не throw type-dialog (replace непустого пула)', async () => {
|
||||
const f = await fillFields({ 'Без расширенного': 'АО Запад' });
|
||||
log(`fillFields CB single: ${JSON.stringify(f.filled)}`);
|
||||
assert.ok(f.filled?.[0]?.ok, 'значение проставлено (без ошибки type dialog)');
|
||||
assert.equal(await fieldValue('СписокПлатформенный'), 'АО Запад', 'поле = АО Запад');
|
||||
});
|
||||
|
||||
await step('одиночный selectValue на поле-списке → делегирует', async () => {
|
||||
const r = await selectValue('Через флажки', 'Бета');
|
||||
log(`selectValue single: ${JSON.stringify(r.selected)}`);
|
||||
assert.deepEqual(r.selected.values, ['Бета'], 'одно значение выбрано');
|
||||
assert.equal(await fieldValue('ЧерезФлажки'), 'Бета', 'поле = Бета');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user