mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 16:14:54 +03:00
feat(web-test): fillTableRow заполняет ячейку-выбор-из-списка через форму выбора
Поле с кнопкой выбора и обработчиком НачалоВыбора (значение выбирается из программного
списка — например колонка Тип в типовой Консоли запросов) раньше заполнялось plain-paste,
который молча откатывался → ok:true/method:direct (ложный успех). Теперь движок детектит
такую ячейку и выбирает значение из формы выбора.
- dom/grid-edit.mjs: readActiveGridCellScript отдаёт buttonKind активной ячейки
(ref/calc/date/choice по кнопке _DLB/_CB и её классу).
- engine/table/row-fill.mjs v1.20: для kind=choice — F4 → pickFromTypeDialog
(скан/Ctrl+F/OK) → method:choice; если после выбора открылась форма значения,
это составная ячейка (нужен {value,type}). Ветка добавлена в Tab-цикл и directEditPick.
- engine/forms/select-value.mjs v1.21: умный dismiss диалога типов на путях
not_found/multiple — Escape только пока диалог открыт, больше не закрывает
исходную форму слепым Escape×3.
- Стенд: строковая колонка-выбор ТипЗначения (НачалоВыбора → ПоказатьВыборЭлемента)
в ДеревоНоменклатуры; тест 16 покрывает method:choice и негатив not_found.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -255,10 +255,22 @@ export function readActiveGridCellScript() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Classify the cell's choice button (if any): ref (_DLB), calc/date (_CB iCalcB/iCalendB),
|
||||
// or bare 'choice' (_CB iCB — value picked from a programmatic list, e.g. НачалоВыбора).
|
||||
let buttonKind = null;
|
||||
const base = f.id.replace(/_i\\d+$/, '');
|
||||
const dlb = document.getElementById(base + '_DLB');
|
||||
const cb = document.getElementById(base + '_CB');
|
||||
if (dlb && dlb.offsetWidth > 0) buttonKind = 'ref';
|
||||
else if (cb && cb.offsetWidth > 0) {
|
||||
if (cb.classList.contains('iCalcB')) buttonKind = 'calc';
|
||||
else if (cb.classList.contains('iCalendB')) buttonKind = 'date';
|
||||
else buttonKind = 'choice';
|
||||
}
|
||||
return {
|
||||
tag: 'INPUT', id: f.id,
|
||||
fullName: f.id.replace(/^form\\d+_/, '').replace(/_i\\d+$/, ''),
|
||||
headerText
|
||||
headerText, buttonKind
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test forms/select-value v1.20 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
|
||||
// web-test forms/select-value v1.21 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -265,6 +265,20 @@ export async function pickFromTypeDialog(formNum, typeName) {
|
||||
return page.evaluate(readTypeDialogVisibleRowsScript(formNum, typeNorm));
|
||||
}
|
||||
|
||||
// Helper: dismiss the type-selection dialog (and any child "Найти") on error.
|
||||
// Escape closes the dialog chain, but a blind Escape×3 cascades into the underlying
|
||||
// form. So press Escape only while THIS type dialog is still present, then stop —
|
||||
// leaving the source form (and cell edit mode) for the caller to handle.
|
||||
async function dismissTypeDialog() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const stillOpen = await page.evaluate(
|
||||
`!!document.getElementById('form${formNum}_OK') || !!document.getElementById('form${formNum}_ValueList')`);
|
||||
if (!stillOpen) break;
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Scan visible rows (fast path — no Ctrl+F needed for small lists)
|
||||
const scan = await readVisibleRows();
|
||||
|
||||
@@ -278,7 +292,7 @@ export async function pickFromTypeDialog(formNum, typeName) {
|
||||
}
|
||||
|
||||
if (scan.matches.length > 1) {
|
||||
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
||||
await dismissTypeDialog();
|
||||
await waitForStable();
|
||||
throw new Error(`selectValue: multiple types match "${typeName}": ${scan.matches.map(m => '"' + m.text + '"').join(', ')}. Specify a more precise type name`);
|
||||
}
|
||||
@@ -307,7 +321,7 @@ export async function pickFromTypeDialog(formNum, typeName) {
|
||||
const findFormNum = await page.evaluate(findChildFormByButtonScript(formNum, 'Find'));
|
||||
|
||||
if (findFormNum === null) {
|
||||
await page.keyboard.press('Escape');
|
||||
await dismissTypeDialog();
|
||||
await waitForStable();
|
||||
throw new Error('selectValue: Ctrl+F did not open "Найти" dialog in type selection');
|
||||
}
|
||||
@@ -320,14 +334,14 @@ export async function pickFromTypeDialog(formNum, typeName) {
|
||||
const afterSearch = await readVisibleRows();
|
||||
|
||||
if (afterSearch.matches.length === 0) {
|
||||
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
||||
await dismissTypeDialog();
|
||||
await waitForStable();
|
||||
throw new Error(`selectValue: type "${typeName}" not found in type selection dialog` +
|
||||
`. Visible: ${(scan.visible || []).join(', ')}`);
|
||||
}
|
||||
|
||||
if (afterSearch.matches.length > 1) {
|
||||
for (let i = 0; i < 3; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(300); }
|
||||
await dismissTypeDialog();
|
||||
await waitForStable();
|
||||
throw new Error(`selectValue: multiple types match "${typeName}": ${afterSearch.matches.map(m => '"' + m.text + '"').join(', ')}. Specify a more precise type name`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test table/row-fill v1.19 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
|
||||
// web-test table/row-fill v1.20 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -245,10 +245,22 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
||||
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, ok: false, error: 'composite_type', message: `Composite type field "${key}" requires {value, type}` };
|
||||
// No type given — treat as a choice cell: the value IS the list item
|
||||
// ("Выбрать тип"). Pick it; if a value form follows, it was genuinely a
|
||||
// composite-value cell that needs {value, type}.
|
||||
try {
|
||||
await pickFromTypeDialog(selForm, info.value);
|
||||
} catch (e) {
|
||||
return { field: key, ok: false, error: 'not_found', message: e.message };
|
||||
}
|
||||
await waitForStable(formNum);
|
||||
const after = await helperDetectNewForm(formNum);
|
||||
if (after !== null) {
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
return { field: key, ok: false, error: 'type_required', message: `Cell "${key}" expects { value, type }` };
|
||||
}
|
||||
return { field: key, ok: true, method: 'choice' };
|
||||
}
|
||||
}
|
||||
const pr = await pickFromSelectionForm(selForm, key, info.value, formNum);
|
||||
@@ -294,6 +306,24 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
||||
// Also check if a selection form already appeared
|
||||
let selForm = await helperDetectNewForm(formNum);
|
||||
if (selForm === null && inInputAfterDblclick) {
|
||||
// Choice cell (bare _CB list-pick) — paste would revert silently; open via F4.
|
||||
const activeCell = await page.evaluate(readActiveGridCellScript());
|
||||
if (activeCell.buttonKind === 'choice') {
|
||||
await page.keyboard.press('F4');
|
||||
let cForm = null;
|
||||
for (let cw = 0; cw < 8; cw++) {
|
||||
await page.waitForTimeout(200);
|
||||
cForm = await helperDetectNewForm(formNum);
|
||||
if (cForm !== null) break;
|
||||
}
|
||||
if (cForm !== null) {
|
||||
const pr = await directEditPick(cForm, key, info);
|
||||
info.filled = true;
|
||||
results.push(pr);
|
||||
continue;
|
||||
}
|
||||
// F4 opened nothing — fall through to paste (best effort)
|
||||
}
|
||||
// Plain text/numeric field — fill via clipboard paste
|
||||
await pasteText(info.value, { confirm: ['Control+a', 'Control+v'] });
|
||||
await page.waitForTimeout(400);
|
||||
@@ -529,6 +559,62 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Choice cell: value is picked from a programmatic list (field with НачалоВыбора →
|
||||
// ПоказатьВыборЭлемента, e.g. a "Выбрать тип" list). Plain paste reverts silently,
|
||||
// so open the choice form via F4 and pick from it.
|
||||
if (cell.buttonKind === 'choice') {
|
||||
await page.keyboard.press('F4');
|
||||
let choiceForm = null;
|
||||
for (let cw = 0; cw < 8; cw++) {
|
||||
await page.waitForTimeout(200);
|
||||
choiceForm = await helperDetectNewForm(formNum);
|
||||
if (choiceForm !== null) break;
|
||||
}
|
||||
if (choiceForm === null) {
|
||||
info.filled = true;
|
||||
results.push({ field: matchedKey, cell: cell.fullName, ok: false,
|
||||
error: 'no_selection_form', message: `Cell "${matchedKey}": F4 did not open a choice form` });
|
||||
await page.keyboard.press('Tab'); await page.waitForTimeout(500);
|
||||
continue;
|
||||
}
|
||||
if (await isTypeDialog(choiceForm)) {
|
||||
try {
|
||||
await pickFromTypeDialog(choiceForm, text);
|
||||
} catch (e) {
|
||||
info.filled = true;
|
||||
results.push({ field: matchedKey, cell: cell.fullName, ok: false,
|
||||
error: 'not_found', message: e.message });
|
||||
await page.keyboard.press('Tab'); await page.waitForTimeout(500);
|
||||
continue;
|
||||
}
|
||||
await waitForStable(formNum);
|
||||
// If a value form opened after the pick, this was a composite-value cell → needs {value, type}
|
||||
const valForm = await helperDetectNewForm(formNum);
|
||||
if (valForm !== null) {
|
||||
await page.keyboard.press('Escape'); await page.waitForTimeout(300);
|
||||
info.filled = true;
|
||||
results.push({ field: matchedKey, cell: cell.fullName, ok: false,
|
||||
error: 'type_required', message: `Cell "${matchedKey}" expects { value, type }` });
|
||||
await page.keyboard.press('Tab'); await page.waitForTimeout(500);
|
||||
continue;
|
||||
}
|
||||
info.filled = true;
|
||||
results.push({ field: matchedKey, cell: cell.fullName, ok: true, method: 'choice', value: text });
|
||||
if ([...pending.values()].every(p => p.filled)) break;
|
||||
await page.keyboard.press('Tab'); await page.waitForTimeout(500);
|
||||
continue;
|
||||
}
|
||||
// F4 opened a regular selection form (reference via CB) — pick from it
|
||||
const pr = await pickFromSelectionForm(choiceForm, matchedKey, text, formNum);
|
||||
info.filled = true;
|
||||
results.push(pr.ok
|
||||
? { field: matchedKey, cell: cell.fullName, ok: true, method: 'form' }
|
||||
: { field: matchedKey, cell: cell.fullName, ok: false, error: pr.error, message: pr.message });
|
||||
if ([...pending.values()].every(p => p.filled)) break;
|
||||
await page.keyboard.press('Tab'); await page.waitForTimeout(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
// === Fill this cell: clipboard paste (trusted event) ===
|
||||
await page.keyboard.press('Control+A');
|
||||
await pasteText(text);
|
||||
|
||||
@@ -812,7 +812,12 @@ export const steps = [
|
||||
{ name: 'Номенклатура', type: 'CatalogRef.Номенклатура', title: 'Номенклатура' },
|
||||
{ name: 'Цена', type: 'Number(15,2)', title: 'Цена' },
|
||||
{ name: 'Картинка', type: 'Boolean', title: 'Картинка' },
|
||||
// Строковая колонка-выбор-из-списка: значение выбирается обработчиком НачалоВыбора
|
||||
// через СписокТипов.ПоказатьВыборЭлемента (как колонка Тип в типовой Консоли запросов).
|
||||
{ name: 'ТипЗначения', type: 'String', title: 'Тип значения' },
|
||||
]},
|
||||
// Список значений для программного выбора (ПоказатьВыборЭлемента).
|
||||
{ name: 'СписокТипов', type: 'ValueList' },
|
||||
],
|
||||
elements: [
|
||||
{ table: 'Дерево', path: 'Дерево', initialTreeView: 'ExpandTopLevel', changeRowSet: true,
|
||||
@@ -824,6 +829,9 @@ export const steps = [
|
||||
{ picField: 'ДеревоКартинка', path: 'Дерево.Картинка', title: 'Картинка', valuesPicture: 'StdPicture.Favorites', loadTransparent: true },
|
||||
// CheckBoxField на тот же булев — для кросс-проверки состояния картинки.
|
||||
{ check: 'ДеревоКартинкаФлаг', path: 'Дерево.Картинка', title: 'Флаг' },
|
||||
// Поле-выбор-из-списка с кнопкой выбора и обработчиком НачалоВыбора.
|
||||
{ input: 'ДеревоТипЗначения', path: 'Дерево.ТипЗначения', title: 'Тип значения',
|
||||
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоТипЗначенияНачалоВыбора' } },
|
||||
]},
|
||||
],
|
||||
},
|
||||
@@ -836,6 +844,10 @@ export const steps = [
|
||||
content: `&НаСервере
|
||||
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
|
||||
\tЗаполнитьУровень(Дерево.ПолучитьЭлементы(), Справочники.Номенклатура.ПустаяСсылка());
|
||||
\tСписокТипов.Добавить("Строка");
|
||||
\tСписокТипов.Добавить("Число");
|
||||
\tСписокТипов.Добавить("Дата");
|
||||
\tСписокТипов.Добавить("Булево");
|
||||
КонецПроцедуры
|
||||
|
||||
&НаСервере
|
||||
@@ -877,6 +889,24 @@ export const steps = [
|
||||
\t\tТекущиеДанные.Картинка = НЕ ТекущиеДанные.Картинка;
|
||||
\tКонецЕсли;
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура ДеревоТипЗначенияНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
|
||||
\tСтандартнаяОбработка = Ложь;
|
||||
\tОписаниеОповещения = Новый ОписаниеОповещения("ТипЗначенияЗавершениеВыбора", ЭтотОбъект);
|
||||
\tСписокТипов.ПоказатьВыборЭлемента(ОписаниеОповещения, НСтр("ru = 'Выбрать тип'"));
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура ТипЗначенияЗавершениеВыбора(ВыбранныйЭлемент, ДополнительныеПараметры) Экспорт
|
||||
\tЕсли ВыбранныйЭлемент = Неопределено Тогда
|
||||
\t\tВозврат;
|
||||
\tКонецЕсли;
|
||||
\tТекущиеДанные = Элементы.Дерево.ТекущиеДанные;
|
||||
\tЕсли ТекущиеДанные <> Неопределено Тогда
|
||||
\t\tТекущиеДанные.ТипЗначения = ВыбранныйЭлемент.Значение;
|
||||
\tКонецЕсли;
|
||||
КонецПроцедуры
|
||||
`,
|
||||
},
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default async function({ navigateLink, clickElement, closeForm, readTable
|
||||
await step('read-roots: на верхнем уровне видны группы (Товары, Услуги, БольшойСписок)', async () => {
|
||||
const t = await readTable('Дерево');
|
||||
log(`columns=${t.columns?.join(',')} rows=${t.rows?.length}`);
|
||||
assert.deepEqual(t.columns, ['Номенклатура', 'Цена', 'Картинка', 'Флаг'], 'колонки: Номенклатура + Цена + Картинка + Флаг');
|
||||
assert.deepEqual(t.columns, ['Номенклатура', 'Цена', 'Картинка', 'Флаг', 'Тип значения'], 'колонки: Номенклатура + Цена + Картинка + Флаг + Тип значения');
|
||||
assert.equal(t.rows.length, 3, '3 корневые строки');
|
||||
const names = t.rows.map(r => r['Номенклатура']);
|
||||
assert.includes(names, 'Товары', 'есть Товары');
|
||||
@@ -61,6 +61,30 @@ export default async function({ navigateLink, clickElement, closeForm, readTable
|
||||
assert.equal(tovar01['Цена'], '1 500,00', 'Цена обновилась до 1 500,00');
|
||||
});
|
||||
|
||||
await step('choice-cell: fillTableRow задаёт ТипЗначения через форму выбора (НачалоВыбора)', async () => {
|
||||
// Колонка-строка с кнопкой выбора + обработчиком НачалоВыбора → СписокТипов.ПоказатьВыборЭлемента
|
||||
// («Выбрать тип»). Plain-paste тут не годится — движок открывает форму выбора и выбирает из списка.
|
||||
const r = await fillTableRow({ ТипЗначения: 'Число' }, { row: 1 });
|
||||
log(`filled: ${JSON.stringify(r.filled)}`);
|
||||
const cell = r.filled?.find(f => f.field === 'ТипЗначения');
|
||||
assert.ok(cell, 'поле ТипЗначения в результате');
|
||||
assert.equal(cell.ok, true, 'ok=true');
|
||||
assert.equal(cell.method, 'choice', 'method=choice (выбор из списка, не direct)');
|
||||
const t = await readTable('Дерево');
|
||||
const tovar01 = t.rows.find(row => row['Номенклатура'] === 'Товар 01');
|
||||
assert.equal(tovar01['Тип значения'], 'Число', 'ТипЗначения = Число');
|
||||
});
|
||||
|
||||
await step('choice-cell-negative: несуществующий тип → ok:false/not_found (форма не закрывается)', async () => {
|
||||
// not_found гасит только диалог выбора типа (умный dismiss), исходная форма остаётся —
|
||||
// следующие шаги (picture) это подтверждают.
|
||||
const r = await fillTableRow({ ТипЗначения: 'Нетакоготипа' }, { row: 1 });
|
||||
const cell = r.filled?.find(f => f.field === 'ТипЗначения');
|
||||
assert.ok(cell, 'поле ТипЗначения в результате');
|
||||
assert.equal(cell.ok, false, 'ok=false для несуществующего типа');
|
||||
assert.equal(cell.error, 'not_found', 'error=not_found');
|
||||
});
|
||||
|
||||
await step('picture: колонка-картинка (pic:0/\'\') + кросс-проверка чекбоксом', async () => {
|
||||
const t = await readTable('Дерево');
|
||||
const t15 = t.rows.find(r => r['Номенклатура'] === 'Товар 15'); // Цена 1500 > 1000 → иконка
|
||||
|
||||
Reference in New Issue
Block a user