mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-10 08:04:56 +03:00
fix(web-test): fillTableRow распознаёт переформатированные число/дату в choice-ячейке
fillChoiceCell определял «прижился ли paste» через normYo(after).includes(text), что ломалось на маск-инпутах: число/дата переформатируются (1234.56 → «1 234,56», группировка неразрывным пробелом, запятая) → includes давал false → ложный уход в F4, где у числа открывался калькулятор и залипал (no_selection_form). Заменил на поведенческий дискриминатор: появился EDD → ссылка (dropdown); инпут изменился на непустое без EDD → редактируемая ячейка (direct); инпут не изменился → НачалоВыбора → F4-форма. + страховка: если F4 открыл не форму выбора (калькулятор/календарь) — Escape и спасение значения. Также в EDD-ветке основного Tab-цикла убран слепой fallback items[0]: при отсутствии exact/includes-совпадения возвращается not_found с очисткой поля, а не подставляется произвольная первая запись автокомплита. Регресс: в стенд (дерево) добавлены choice-колонки Число/Дата и булево-поле-ввода; в 16-tree-form — шаги choice-number/choice-date/bool-input. Полный регресс: 22 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// web-test table/row-fill v1.22 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
|
||||
// web-test table/row-fill v1.23 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -58,15 +58,46 @@ async function fillChoiceCell(formNum, text, { type = null, fieldLabel = '' } =
|
||||
if (before && norm(before).includes(norm(text))) {
|
||||
return { ok: true, method: 'skip', value: before };
|
||||
}
|
||||
// Try direct input; poll for the input value to settle on the pasted text (editable cell).
|
||||
// Paste, then poll. Three outcomes, distinguished BEHAVIORALLY (not by value equality):
|
||||
// (1) EDD autocomplete appears → reference/list cell → pick from the dropdown;
|
||||
// (2) input changes to non-empty, no EDD → editable cell → leave value, method 'direct';
|
||||
// (3) input unchanged (rejected) → НачалоВыбора pick-from-list → F4 selection form.
|
||||
// A value-equality check on `after` is UNRELIABLE: numeric/date masks reformat the pasted
|
||||
// text (grouping nbsp, decimal comma, padding) — e.g. "1234.56" → "1 234,56", "0,000"
|
||||
// baseline. So we test "did the input change to non-empty" + "no autocomplete", never
|
||||
// "does after contain text" (that false-negatives on reformatting → F4 → stray calculator).
|
||||
await pasteText(text, { confirm: ['Control+a', 'Control+v'] });
|
||||
let after = before, stuck = false;
|
||||
let after = before, changed = false, eddSeen = false;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await page.waitForTimeout(100);
|
||||
if (await isEddVisible()) { eddSeen = true; break; }
|
||||
after = await page.evaluate(`document.activeElement?.value || ''`);
|
||||
if (after !== before && norm(after).includes(norm(text))) { stuck = true; break; }
|
||||
if (after !== before && after !== '') changed = true;
|
||||
}
|
||||
|
||||
if (eddSeen) {
|
||||
// Reference/list cell — pick a MATCHING item from the autocomplete. Only accept an
|
||||
// exact (parenthetical-stripped) or substring match; never blind-pick items[0] — for a
|
||||
// non-existent value 1C still lists unrelated entries, and picking the first silently
|
||||
// writes the wrong reference. No match → fall through to the F4 selection form, which
|
||||
// searches the full list and returns not_found if the value is truly absent.
|
||||
const edd = await readEdd();
|
||||
const items = (edd.items || []).map(i => i.name)
|
||||
.filter(i => !/^Создать[\s:]/.test(i) && !/не найдено/i.test(i) && !/показать все/i.test(i));
|
||||
const tgt = norm(text);
|
||||
const pick = items.find(i => norm(i.replace(/\s*\([^)]*\)\s*$/, '')) === tgt)
|
||||
|| items.find(i => norm(i).includes(tgt));
|
||||
if (pick) {
|
||||
await clickEddItemViaDispatch(pick);
|
||||
await waitForStable();
|
||||
return { ok: true, method: 'dropdown', value: pick.replace(/\s*\([^)]*\)\s*$/, '') };
|
||||
}
|
||||
// No matching item — dismiss the autocomplete and fall through to the F4 selection form.
|
||||
await page.keyboard.press('Escape'); await page.waitForTimeout(200);
|
||||
} else if (changed) {
|
||||
// Editable cell — value lives in the INPUT; caller's Tab / end-of-row commit persists it.
|
||||
return { ok: true, method: 'direct', value: after };
|
||||
}
|
||||
if (stuck) return { ok: true, method: 'direct', value: text };
|
||||
|
||||
// Text rejected (pick-from-list cell) — nothing typed to clear (field is not text-editable).
|
||||
// Dismiss any autocomplete hint, then open the choice form via F4.
|
||||
@@ -79,6 +110,15 @@ async function fillChoiceCell(formNum, text, { type = null, fieldLabel = '' } =
|
||||
if (choiceForm !== null) break;
|
||||
}
|
||||
if (choiceForm === null) {
|
||||
// F4 safety net: on an editable numeric/date cell mis-routed here, F4 opens a
|
||||
// calculator/calendar (NOT a selection form). Close it — never leave the popup open
|
||||
// (it blocks the UI) — and salvage: if the cell now holds a value, count it as 'direct'.
|
||||
if (await findOpenPopup()) {
|
||||
await page.keyboard.press('Escape');
|
||||
for (let dw = 0; dw < 4; dw++) { await page.waitForTimeout(150); if (!(await findOpenPopup())) break; }
|
||||
const nowVal = await page.evaluate(`document.activeElement?.value || ''`);
|
||||
if (nowVal && nowVal !== before) return { ok: true, method: 'direct', value: nowVal };
|
||||
}
|
||||
return { ok: false, error: 'no_selection_form', message: `Cell "${fieldLabel || text}": F4 did not open a choice form` };
|
||||
}
|
||||
if (await isTypeDialog(choiceForm)) {
|
||||
@@ -681,14 +721,28 @@ export async function fillTableRow(fields, { tab, add, row, table, scroll } = {}
|
||||
let pick = realItems.find(i =>
|
||||
normYo(i.replace(/\s*\([^)]*\)\s*$/, '').toLowerCase()) === tgt);
|
||||
if (!pick) pick = realItems.find(i => normYo(i.toLowerCase()).includes(tgt));
|
||||
if (!pick) pick = realItems[0];
|
||||
|
||||
// Click EDD item via dispatchEvent (bypasses div.surface overlay)
|
||||
await clickEddItemViaDispatch(pick);
|
||||
await waitForStable();
|
||||
info.filled = true;
|
||||
results.push({ field: matchedKey, cell: cell.fullName, ok: true,
|
||||
method: 'dropdown', value: pick.replace(/\s*\([^)]*\)\s*$/, '') });
|
||||
if (pick) {
|
||||
// Click EDD item via dispatchEvent (bypasses div.surface overlay)
|
||||
await clickEddItemViaDispatch(pick);
|
||||
await waitForStable();
|
||||
info.filled = true;
|
||||
results.push({ field: matchedKey, cell: cell.fullName, ok: true,
|
||||
method: 'dropdown', value: pick.replace(/\s*\([^)]*\)\s*$/, '') });
|
||||
} else {
|
||||
// EDD listed items but NONE matches the requested value. Do NOT blind-pick the
|
||||
// first item — when the typed text has no hit, 1C still shows unrelated entries
|
||||
// (recent/full list), so items[0] would silently write the wrong reference.
|
||||
// Dismiss, clear the typed text, report not_found.
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('Control+A');
|
||||
await page.keyboard.press('Delete');
|
||||
await page.waitForTimeout(200);
|
||||
info.filled = true;
|
||||
results.push({ field: matchedKey, cell: cell.fullName, ok: false,
|
||||
error: 'not_found', message: `No match for "${text}" in autocomplete` });
|
||||
}
|
||||
} else {
|
||||
// Only "Создать:" items — value not found in autocomplete
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
@@ -818,6 +818,14 @@ export const steps = [
|
||||
// Редактируемая строковая колонка: у поля есть кнопка выбора, но НачалоВыбора пустой
|
||||
// (F4 ничего не открывает), текст вводится напрямую — модель ячейки «Значение» Консоли запросов.
|
||||
{ name: 'РедактируемаяСтрока', type: 'String', title: 'Редактируемая строка' },
|
||||
// Редактируемые choice-ячейки Число и Дата (та же модель «Значение» КЗ): кнопка выбора +
|
||||
// пустой НачалоВыбора, текст вводится напрямую и ПЕРЕФОРМАТИРУЕТСЯ маск-инпутом
|
||||
// (1234.56 → «1 234,56»). Регресс-guard для fillChoiceCell — раньше includes-проверка
|
||||
// рвалась о переформатирование → ложное F4 → калькулятор.
|
||||
{ name: 'РедактируемоеЧисло', type: 'Number(15,2)', title: 'Редактируемое число' },
|
||||
{ name: 'РедактируемаяДата', type: 'date', title: 'Редактируемая дата' },
|
||||
// Булева колонка-флажок (отдельно от Картинка) — для fillTableRow toggle на дереве.
|
||||
{ name: 'Булево', type: 'Boolean', title: 'Булево' },
|
||||
]},
|
||||
// Список значений для программного выбора (ПоказатьВыборЭлемента).
|
||||
{ name: 'СписокТипов', type: 'ValueList' },
|
||||
@@ -841,6 +849,17 @@ export const steps = [
|
||||
// кнопка iCB есть, F4 ничего не открывает, текст редактируется напрямую (модель «Значение»).
|
||||
{ input: 'ДеревоРедактируемаяСтрока', path: 'Дерево.РедактируемаяСтрока', title: 'Редактируемая строка',
|
||||
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоРедактируемаяСтрокаНачалоВыбора' } },
|
||||
// Редактируемые choice-ячейки Число/Дата: кнопка iCB + пустой НачалоВыбора → текст
|
||||
// редактируется напрямую, значение переформатируется маск-инпутом (модель «Значение» КЗ).
|
||||
{ input: 'ДеревоРедактируемоеЧисло', path: 'Дерево.РедактируемоеЧисло', title: 'Редактируемое число',
|
||||
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоРедактируемоеЧислоНачалоВыбора' } },
|
||||
{ input: 'ДеревоРедактируемаяДата', path: 'Дерево.РедактируемаяДата', title: 'Редактируемая дата',
|
||||
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоРедактируемаяДатаНачалоВыбора' } },
|
||||
// Булево как ПОЛЕ ВВОДА с кнопкой выбора (не флажок): в ячейке выбор Да/Нет —
|
||||
// fillTableRow идёт через dropdown-путь (как «Значение» типа Булево в Консоли
|
||||
// запросов), не toggle. Кнопка iCB + пустой НачалоВыбора — единая модель «Значение».
|
||||
{ input: 'ДеревоБулево', path: 'Дерево.Булево', title: 'Булево',
|
||||
choiceButton: true, on: ['StartChoice'], handlers: { StartChoice: 'ДеревоБулевоНачалоВыбора' } },
|
||||
]},
|
||||
],
|
||||
},
|
||||
@@ -926,6 +945,25 @@ export const steps = [
|
||||
\t// Текст вводится напрямую — модель ячейки «Значение» типовой Консоли запросов.
|
||||
\tСтандартнаяОбработка = Ложь;
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура ДеревоРедактируемоеЧислоНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
|
||||
\t// Пустой обработчик — число редактируется напрямую (модель «Значение» КЗ).
|
||||
\tСтандартнаяОбработка = Ложь;
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура ДеревоРедактируемаяДатаНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
|
||||
\t// Пустой обработчик — дата редактируется напрямую (модель «Значение» КЗ).
|
||||
\tСтандартнаяОбработка = Ложь;
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура ДеревоБулевоНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
|
||||
\t// Пустой обработчик: кнопка выбора есть, F4 ничего не открывает; значение задаётся
|
||||
\t// штатным списком Да/Нет поля ввода булева (модель «Значение» типа Булево в КЗ).
|
||||
\tСтандартнаяОбработка = Ложь;
|
||||
КонецПроцедуры
|
||||
`,
|
||||
},
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ export const timeout = 90000;
|
||||
// + дискриминатор choice-ячейки (fillChoiceCell): ДеревоРедактируемаяСтрока (кнопка iCB,
|
||||
// пустой НачалоВыбора, текст редактируется → method:'direct') vs ДеревоТипЗначения
|
||||
// (РедактированиеТекста=Ложь, текст отвергается → форма выбора, method:'choice').
|
||||
// + редактируемые choice-ячейки Число/Дата (РедактируемоеЧисло/РедактируемаяДата): маск-инпут
|
||||
// переформатирует значение (1234.56 → «1 234,56») — регресс на баг с ложным F4→калькулятор.
|
||||
// + булево как поле ввода (Булево, InputField, не флажок): выбор Да/Нет через dropdown-путь.
|
||||
|
||||
export default async function({ navigateLink, clickElement, closeForm, readTable, fillTableRow, assert, step, log }) {
|
||||
|
||||
@@ -27,7 +30,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, 'Товары', 'есть Товары');
|
||||
@@ -119,6 +122,50 @@ export default async function({ navigateLink, clickElement, closeForm, readTable
|
||||
assert.equal(cell.error, 'not_found', 'error=not_found');
|
||||
});
|
||||
|
||||
await step('choice-number: редактируемая choice-ячейка Число — paste переформатируется, method:direct', async () => {
|
||||
// РедактируемоеЧисло — Number-колонка с кнопкой выбора (iCB) и пустым НачалоВыбора (модель
|
||||
// «Значение» КЗ). Маск-инпут ПЕРЕФОРМАТИРУЕТ «1234.56» → «1 234,56» (nbsp-разделитель, запятая).
|
||||
// Регресс на баг, где includes-проверка рвалась о переформатирование → ложное F4 → калькулятор.
|
||||
// Теперь дискриминатор поведенческий (инпут изменился + нет EDD → direct), формат не важен.
|
||||
const r = await fillTableRow({ 'Редактируемое число': '1234.56' }, { row: 1 });
|
||||
log(`filled: ${JSON.stringify(r.filled)}`);
|
||||
const cell = r.filled?.find(f => /Редактируемое число/i.test(f.field));
|
||||
assert.ok(cell, 'поле Редактируемое число в результате');
|
||||
assert.equal(cell.ok, true, 'ok=true');
|
||||
assert.equal(cell.method, 'direct', 'method=direct (числовой маск-инпут, без F4/калькулятора)');
|
||||
const t = await readTable('Дерево');
|
||||
const tovar01 = t.rows.find(row => row['Номенклатура'] === 'Товар 01');
|
||||
// 1С web использует неразрывный пробел как разделитель разрядов — убираем все пробелы перед сравнением.
|
||||
assert.equal((tovar01['Редактируемое число'] || '').replace(/[\s\u00A0]/g, ''), '1234,56', 'число переформатировано в 1234,56');
|
||||
});
|
||||
|
||||
await step('choice-date: редактируемая choice-ячейка Дата — method:direct', async () => {
|
||||
// РедактируемаяДата — Date-колонка с кнопкой выбора и пустым НачалоВыбора. Та же модель «Значение».
|
||||
const r = await fillTableRow({ 'Редактируемая дата': '15.06.2025' }, { row: 1 });
|
||||
log(`filled: ${JSON.stringify(r.filled)}`);
|
||||
const cell = r.filled?.find(f => /Редактируемая дата/i.test(f.field));
|
||||
assert.ok(cell, 'поле Редактируемая дата в результате');
|
||||
assert.equal(cell.ok, true, 'ok=true');
|
||||
assert.equal(cell.method, 'direct', 'method=direct');
|
||||
const t = await readTable('Дерево');
|
||||
const tovar01 = t.rows.find(row => row['Номенклатура'] === 'Товар 01');
|
||||
assert.equal(tovar01['Редактируемая дата'], '15.06.2025', 'дата записана');
|
||||
});
|
||||
|
||||
await step('bool-input: булева ячейка-поле-ввода (не флажок) заполняется выбором Да', async () => {
|
||||
// ДеревоБулево — InputField на булевом пути (НЕ CheckBoxField) с кнопкой выбора (iCB) и
|
||||
// пустым НачалоВыбора: в ячейке выбор Да/Нет, fillTableRow идёт через dropdown-путь, а не
|
||||
// toggle. Покрывает булево как поле ввода (модель «Значение» типа Булево в Консоли запросов).
|
||||
const r = await fillTableRow({ 'Булево': 'Да' }, { row: 1 });
|
||||
log(`filled: ${JSON.stringify(r.filled)}`);
|
||||
const cell = r.filled?.find(f => /Булево/i.test(f.field));
|
||||
assert.ok(cell, 'поле Булево в результате');
|
||||
assert.equal(cell.ok, true, 'ok=true');
|
||||
const t = await readTable('Дерево');
|
||||
const tovar01 = t.rows.find(row => row['Номенклатура'] === 'Товар 01');
|
||||
assert.equal(tovar01['Булево'], 'Да', 'Булево = Да');
|
||||
});
|
||||
|
||||
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