From 3a7f1c17637b47fa08eabedd27660ae693f5f19c Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 29 Jun 2026 12:55:34 +0300 Subject: [PATCH] =?UTF-8?q?fix(web-test):=20=D0=B7=D0=B0=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20data-=D1=8F=D1=87=D0=B5=D0=B5?= =?UTF-8?q?=D0=BA=20=D0=B1=D0=B5=D0=B7=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B3=D1=80=D0=B8=D0=B4=D0=B0=20+=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=B3=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=87=D0=B5=D0=BA=D0=B1?= =?UTF-8?q?=D0=BE=D0=BA=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Два фикса в fillTableRow на гридах: 1. Безголовые гриды (без .gridHead): правка data-ячейки (число/ссылка) молча возвращала filled:[]. В режиме редактирования INPUT рендерится в оверлее .inputs ВНЕ .gridBox, поэтому readActiveGridCell не доставал colindex через предков → синтетическое имя Колонка{N} не матчилось в Tab-цикле. Теперь colindex резолвится по x-координате INPUT относительно ячеек тела грида — тем же приёмом, что и шапочная ветка, через единый synthHeaderlessColumns. 2. Чекбокс-ячейки: безусловный первичный клик по центру ячейки тогглил чекбокс ДО checkbox-логики, которая затем «доправляла» значение обратно — лишний клик и видимое мигание (снимаем→ставим) при установке в уже текущее значение. Детект чекбокса (findCheckboxAtPoint, читает состояние из DOM без режима редактирования) перенесён ДО первичного клика: кликаем по иконке только когда текущее состояние != желаемого (0 кликов на идемпотентном тоггле). Затрагивало любые таблицы, не только безголовые. Тест 21-headerless расширен шагами на заполнение data-ячеек (число direct, ссылка dropdown) и клик по data-колонке (через API, без raw DOM). Полный регресс web-test: 25 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/web-test/scripts/dom/grid-edit.mjs | 15 ++++-- .../scripts/engine/table/row-fill.mjs | 49 ++++++++++--------- tests/web-test/21-headerless.test.mjs | 28 ++++++++++- 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/.claude/skills/web-test/scripts/dom/grid-edit.mjs b/.claude/skills/web-test/scripts/dom/grid-edit.mjs index edcff9d6..01450cc6 100644 --- a/.claude/skills/web-test/scripts/dom/grid-edit.mjs +++ b/.claude/skills/web-test/scripts/dom/grid-edit.mjs @@ -1,4 +1,4 @@ -// web-test dom/grid-edit v1.1 — DOM scripts for row-fill (grid edit-time operations) +// web-test dom/grid-edit v1.2 — DOM scripts for row-fill (grid edit-time operations) // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills // import { HEADERLESS_GRID_FN } from './_shared.mjs'; @@ -281,9 +281,16 @@ export function readActiveGridCellScript() { } } if (!head) { - // Headerless: resolve the editing column name from the cell's colindex via synth. - let box = f; while (box && !box.classList?.contains('gridBox')) box = box.parentElement; - const ci = box?.getAttribute('colindex'); + // Headerless: the editing INPUT is rendered in an overlay (.inputs) OUTSIDE + // the .gridBox, so walking ancestors for colindex fails. Resolve colindex by + // matching the input's x against the body cells (same idea as the headed branch). + const bl = grid.querySelector('.gridBody .gridLine'); + let ci = null; + if (bl) for (const b of bl.children) { + if (b.offsetWidth === 0) continue; + const br = b.getBoundingClientRect(); + if (fr.x >= br.x && fr.x < br.x + br.width) { ci = b.getAttribute('colindex'); break; } + } if (ci != null) { const sc = synthHeaderlessColumns(grid).find(c => c.kind === 'data' && c.colindex === ci); if (sc) headerText = sc.name; diff --git a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs index e940089e..8420159e 100644 --- a/.claude/skills/web-test/scripts/engine/table/row-fill.mjs +++ b/.claude/skills/web-test/scripts/engine/table/row-fill.mjs @@ -1,4 +1,4 @@ -// web-test table/row-fill v1.24 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений. +// web-test table/row-fill v1.25 — fillTableRow — заполнение строки табличной части/списка через Tab-навигацию и попутный выбор значений. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { @@ -236,6 +236,32 @@ export async function fillTableRow(fields, { tab, add, row, table, scroll } = {} } } + // Checkbox cell? Detect BEFORE any click. Clicking the cell center toggles the + // checkbox, so an unconditional click here would flip an already-correct value + // (visible flicker + redundant write — снимаем→ставим). findCheckboxAtPoint reads + // the state straight from the DOM (no edit mode needed), so we branch first and + // click the icon only when the current state differs from the desired one. + if (firstVal0 !== '') { + const checkboxInfo = await page.evaluate(findCheckboxAtPointScript(cellCoords.x, cellCoords.y)); + if (checkboxInfo !== null) { + const desired = ['true', 'да', '1', 'yes'].includes(String(firstVal0).toLowerCase().trim()); + if (checkboxInfo.checked !== desired) { + await page.mouse.click(checkboxInfo.x, checkboxInfo.y); + await page.waitForTimeout(300); + } + const results = [{ field: firstKey0, ok: true, method: 'toggle', value: desired }]; + await waitForStable(formNum); + // If more fields remain, process them on the same row + const remaining = { ...fields }; + delete remaining[firstKey0]; + if (Object.keys(remaining).length > 0) { + const more = await fillTableRow(remaining, { row, table }); + results.push(...more.filled); + } + return returnFormState({ filled: results }); + } + } + // Click first (tree grids enter edit on single click; dblclick toggles expand/collapse). // Then escalate: dblclick → F4 if needed. await page.mouse.click(cellCoords.x, cellCoords.y); @@ -271,27 +297,6 @@ export async function fillTableRow(fields, { tab, add, row, table, scroll } = {} return returnFormState({ filled: results }); } - // Check if clicked cell is a checkbox (toggle-on-click, no edit mode) - const checkboxInfo = await page.evaluate(findCheckboxAtPointScript(cellCoords.x, cellCoords.y)); - if (checkboxInfo !== null) { - // Checkbox cell found — click directly on the checkbox icon (not cell center) - const desired = ['true', 'да', '1', 'yes'].includes(String(firstVal0).toLowerCase().trim()); - if (checkboxInfo.checked !== desired) { - await page.mouse.click(checkboxInfo.x, checkboxInfo.y); - await page.waitForTimeout(300); - } - const results = [{ field: firstKey0, ok: true, method: 'toggle', value: desired }]; - await waitForStable(formNum); - // If more fields remain, process them on the same row - const remaining = { ...fields }; - delete remaining[firstKey0]; - if (Object.keys(remaining).length > 0) { - const more = await fillTableRow(remaining, { row, table }); - results.push(...more.filled); - } - return returnFormState({ filled: results }); - } - let inEdit = false; let directEditForm = null; for (let dw = 0; dw < 4; dw++) { diff --git a/tests/web-test/21-headerless.test.mjs b/tests/web-test/21-headerless.test.mjs index 44f992c4..26daa39c 100644 --- a/tests/web-test/21-headerless.test.mjs +++ b/tests/web-test/21-headerless.test.mjs @@ -6,7 +6,9 @@ export const timeout = 180000; // - БезшапочнаяТаблица (ValueTable, отдельная колонка-чекбокс) // - МножественныйВыбор → Через флажки (марк-список, комбинированная ячейка галка+текст) // Деривация колонок едина (synthHeaderlessColumns): Колонка{N} + (checkbox), привязка по colindex. -// Правка data-ячейки на безголовом ValueTable — follow-up (edit-Tab-loop row-fill), здесь не тестируется. +// Правка data-ячейки на безголовом ValueTable (число/ссылка через edit-Tab-loop) — тоже покрыта: +// в режиме редактирования INPUT рендерится в оверлее вне .gridBox, colindex резолвится по +// x-координате относительно ячеек тела (grid-edit v1.2), иначе synth-имя Колонка{N} не матчилось. export default async function({ navigateLink, clickElement, fillTableRow, readTable, getFormState, wait, assert, step, log, closeForm @@ -65,6 +67,30 @@ export default async function({ assert.equal(r1['(checkbox)'], want, `клик переключил галку Позиция 003 в ${want}`); }); + await step('fillTableRow: правка числовой data-ячейки Колонка2 по индексу (method=direct)', async () => { + const r = await fillTableRow({ 'Колонка2': '777' }, { row: 1 }); + log(`filled: ${JSON.stringify(r.filled)}`); + assert.ok(r.filled?.[0]?.ok && r.filled[0].method === 'direct', 'method=direct, ok'); + const t = await readTable({ maxRows: 5 }); + assert.ok(t.rows[1]['Колонка2'].startsWith('777'), `Колонка2 строки 1 = 777, got ${t.rows[1]['Колонка2']}`); + }); + + await step('fillTableRow: правка ссылочной data-ячейки Колонка1 по фильтру (выбор из списка)', async () => { + const r = await fillTableRow({ 'Колонка1': 'Позиция 001' }, { row: { 'Колонка1': 'Позиция 003' } }); + log(`filled: ${JSON.stringify(r.filled)}`); + assert.ok(r.filled?.[0]?.ok, 'ok'); + assert.ok(['dropdown', 'form'].includes(r.filled[0].method), `method dropdown|form, got ${r.filled[0].method}`); + const t = await readTable({ maxRows: 5 }); + assert.equal(t.rows.filter(x => x['Колонка1'] === 'Позиция 001').length, 2, 'две строки Позиция 001 (была 001 + перезаписанная 003)'); + }); + + await step('clickElement: клик по data-ячейке резолвит синтетическую колонку Колонка3', async () => { + const r = await clickElement({ row: { 'Колонка1': 'Позиция 002' }, column: 'Колонка3' }); + log(`clicked: ${JSON.stringify(r.clicked)}`); + assert.equal(r.clicked?.kind, 'gridCell', 'клик по ячейке грида'); + assert.equal(r.clicked?.column, 'Колонка3', 'резолвнутая колонка — синтетическая Колонка3'); + }); + // ── Марк-список (комбинированная ячейка галка+текст в одной .gridBox) ──────── await step('марк-список: readTable → (checkbox) + Колонка1, расщепление одной ячейки', async () => { await navigateLink('Обработка.МножественныйВыбор');