From ffb380187fc1322de32655c621db6b626b9e5e56 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 1 Jun 2026 20:00:46 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20exact-match=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B5=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D0=B0=20=D0=B2=20pickFromTypeDialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Диалог выбора типа матчил по подстроке и падал «multiple types match», даже когда точное совпадение присутствовало в выдаче (напр. поиск «Контрагент» давал «Банковская карта контрагента», «Договор с контрагентом», …, «Контрагент» — и движок ругался, хотя точная строка была видна). pickFromTypeDialog теперь предпочитает точное совпадение (resolveExact: единственный матч, либо единственная строка, равная искомому имени после нормализации регистра/ё) — кликает именно её и жмёт OK. Применяется и в scan-пути (мелкие списки), и после Ctrl+F (большие виртуальные списки). Добавлен ограниченный скролл-скан (PageDown ×3) на случай, когда точная строка чуть ниже первого окна. Ошибка неоднозначности остаётся, только если единственного точного совпадения действительно нет. Стенд: в СписокТипов добавлен подстрочный дубль «Дата документа» рядом с «Дата» для детерминированной проверки exact-match. Тест 16-tree-form покрывает scan-путь (выбирается точное «Дата»). Проверено: регресс web-test 22/22, живой E2E на типовой Консоли запросов (ссылочный тип через Ctrl+F + примитив без регресса). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../scripts/engine/forms/select-value.mjs | 90 ++++++++++++------- .../integration/build-webtest-config.test.mjs | 3 + tests/web-test/16-tree-form.test.mjs | 14 +++ 3 files changed, 73 insertions(+), 34 deletions(-) diff --git a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs index afd2dab2..22bec90b 100644 --- a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs +++ b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs @@ -1,4 +1,4 @@ -// web-test forms/select-value v1.21 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers. +// web-test forms/select-value v1.22 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { @@ -279,33 +279,44 @@ export async function pickFromTypeDialog(formNum, typeName) { } } - // Step 1: Scan visible rows (fast path — no Ctrl+F needed for small lists) - const scan = await readVisibleRows(); - - if (scan.matches.length === 1) { - // Single match — click to select, then OK - await page.mouse.click(scan.matches[0].x, scan.matches[0].y); + // Exact-match preference: substring search can surface several types that merely CONTAIN the + // requested name (e.g. "Контрагент" → "Банковская карта контрагента", "Договор с контрагентом", + // …, "Контрагент"). Prefer the row equal to the requested name; only the absence of a single + // exact match among multiple substring hits is a genuine ambiguity. + function resolveExact(matches) { + if (!matches || matches.length === 0) return null; + if (matches.length === 1) return matches[0]; + const exact = matches.filter(m => normYo((m.text || '').toLowerCase()) === typeNorm); + return exact.length === 1 ? exact[0] : null; + } + async function selectRowAndOk(row) { + await page.mouse.click(row.x, row.y); await page.waitForTimeout(200); await page.click(`#form${formNum}_OK`, { force: true }); await page.waitForTimeout(ACTION_WAIT); - return; + } + // Focus the grid via evaluate (does NOT punch through the modal overlay like page.click). + async function focusGrid() { + await page.evaluate(`(() => { + const grid = document.getElementById('form${formNum}_ValueList'); + if (!grid) return; + const body = grid.querySelector('.gridBody'); + if (body) body.focus(); else grid.focus(); + })()`); } + // Step 1: Scan visible rows (fast path — no Ctrl+F needed for small lists) + const scan = await readVisibleRows(); + const scanPick = resolveExact(scan.matches); + if (scanPick) { await selectRowAndOk(scanPick); return; } if (scan.matches.length > 1) { 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`); } - // Step 2: Not found in visible rows — use Ctrl+F (virtual grid may have more items) - - // Focus the grid via evaluate (does NOT punch through modal like page.click) - await page.evaluate(`(() => { - const grid = document.getElementById('form${formNum}_ValueList'); - if (!grid) return; - const body = grid.querySelector('.gridBody'); - if (body) body.focus(); else grid.focus(); - })()`); + // Step 2: Not in visible rows — Ctrl+F jumps near the match in the large virtual list. + await focusGrid(); await page.waitForTimeout(300); // Ctrl+F to open "Найти" dialog @@ -326,29 +337,40 @@ export async function pickFromTypeDialog(formNum, typeName) { throw new Error('selectValue: Ctrl+F did not open "Найти" dialog in type selection'); } - // Click "Найти" — search is client-side (no server round-trip), 500ms is enough + // Click "Найти" — search is client-side (no server round-trip) await page.click(`#form${findFormNum}_Find`, { force: true }); - await page.waitForTimeout(500); - // Re-read visible rows after search scrolled to match - const afterSearch = await readVisibleRows(); + // "Найти" positions at the first match; the exact row is at or just below it. Read, and if the + // exact match is not yet in view, PageDown a few times (bounded) — virtualised grid, scrollTop + // stays 0 but the visible window changes. Poll each window for matches to settle. + let resolved = null, lastMatches = [], sawMatches = false; + for (let pageStep = 0; pageStep <= 3; pageStep++) { + if (pageStep > 0) { await focusGrid(); await page.keyboard.press('PageDown'); } + let v = null; + for (let w = 0; w < 5; w++) { + await page.waitForTimeout(200); + v = await readVisibleRows(); + if (v.matches.length) break; + } + if (v && v.matches.length) { + sawMatches = true; + lastMatches = v.matches; + resolved = resolveExact(v.matches); + if (resolved) break; + // matches present but no single exact in this window — scroll to look just below + } else if (sawMatches) { + break; // scrolled past the matches without finding an exact one + } + } + if (resolved) { await selectRowAndOk(resolved); return; } - if (afterSearch.matches.length === 0) { - await dismissTypeDialog(); - await waitForStable(); + await dismissTypeDialog(); + await waitForStable(); + if (!sawMatches) { throw new Error(`selectValue: type "${typeName}" not found in type selection dialog` + `. Visible: ${(scan.visible || []).join(', ')}`); } - - if (afterSearch.matches.length > 1) { - 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`); - } - - // Click OK on type dialog via page.click({force:true}) — bypasses "Найти" modal - await page.click(`#form${formNum}_OK`, { force: true }); - await page.waitForTimeout(ACTION_WAIT); + throw new Error(`selectValue: multiple types match "${typeName}": ${lastMatches.map(m => '"' + m.text + '"').join(', ')}. Specify a more precise type name`); } /** diff --git a/tests/skills/integration/build-webtest-config.test.mjs b/tests/skills/integration/build-webtest-config.test.mjs index b2b66d65..c464863c 100644 --- a/tests/skills/integration/build-webtest-config.test.mjs +++ b/tests/skills/integration/build-webtest-config.test.mjs @@ -857,6 +857,9 @@ export const steps = [ \tСписокТипов.Добавить("Число"); \tСписокТипов.Добавить("Дата"); \tСписокТипов.Добавить("Булево"); +\t// Подстрочный дубль «Дата» — для проверки exact-match в pickFromTypeDialog: +\t// поиск «Дата» даёт 2 совпадения, движок должен выбрать точное «Дата», не «Дата документа». +\tСписокТипов.Добавить("Дата документа"); КонецПроцедуры &НаСервере diff --git a/tests/web-test/16-tree-form.test.mjs b/tests/web-test/16-tree-form.test.mjs index e2913804..3481fb11 100644 --- a/tests/web-test/16-tree-form.test.mjs +++ b/tests/web-test/16-tree-form.test.mjs @@ -95,6 +95,20 @@ export default async function({ navigateLink, clickElement, closeForm, readTable assert.equal(tovar01['Тип значения'], 'Число', 'ТипЗначения = Число'); }); + await step('choice-exact: при подстрочной неоднозначности выбирается точное совпадение', async () => { + // СписокТипов содержит «Дата» и «Дата документа». Поиск «Дата» даёт 2 подстрочных + // совпадения — pickFromTypeDialog должен предпочесть ТОЧНОЕ «Дата», а не ругаться + // на неоднозначность и не выбрать «Дата документа» (Проблема 2 из bug-report). + const r = await fillTableRow({ ТипЗначения: 'Дата' }, { row: 1 }); + const cell = r.filled?.find(f => f.field === 'ТипЗначения'); + assert.ok(cell, 'поле ТипЗначения в результате'); + assert.equal(cell.ok, true, 'ok=true (exact-match разрешил неоднозначность)'); + assert.equal(cell.method, 'choice', 'method=choice'); + 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) это подтверждают.