From b00289e62c3aeb95b6c0854e3a57cf992b6e9c56 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Mar 2026 10:51:20 +0300 Subject: [PATCH 1/5] feat(web-test): expand alias, tree expand fix, fillTableRow checkbox support - clickElement: add `expand` option (alias for `toggle`) for tree expand/collapse - clickElement: fallback to dblclick when tree +/- icon not found (was NumpadAdd) - dom.mjs: search [tree="true"] in entire line, not just first imgBox (fixes trees with checkbox column before tree column) - fillTableRow: detect checkbox cells after first click, return immediately without escalation (dblclick/F4). Checkbox state detected via .select class - SKILL.md: document `expand` instead of `toggle` Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/web-test/SKILL.md | 8 ++--- .claude/skills/web-test/scripts/browser.mjs | 37 ++++++++++++++++----- .claude/skills/web-test/scripts/dom.mjs | 2 +- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 487e16d4..d089c512 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -198,7 +198,7 @@ Sections + all open tabs. ### Actions -#### `clickElement(text, { dblclick?, table?, toggle? })` → form state +#### `clickElement(text, { dblclick?, table?, expand? })` → form state Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match). - `table` — scope button search to a specific grid's command panel (by name from `tables[]`): @@ -215,10 +215,10 @@ Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match). // r.submenu = ['Расширенный поиск', 'Настройки', ...] await clickElement('Расширенный поиск'); ``` -- **Tree nodes**: default click = **select** (highlight row). Use `{ toggle: true }` to **expand/collapse**: +- **Tree nodes**: default click = **select** (highlight row). Use `{ expand: true }` to **expand/collapse**: ```js - await clickElement('ИСУ ФХД'); // select row - await clickElement('ИСУ ФХД', { toggle: true }); // expand/collapse + await clickElement('ИСУ ФХД'); // select row + await clickElement('ИСУ ФХД', { expand: true }); // expand/collapse ``` #### `fillFields({ name: value })` → `{ filled, form }` diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index d82e9e2c..4f66dfcf 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -1,4 +1,4 @@ -// web-test browser v1.3 — Playwright browser management for 1C web client +// web-test browser v1.4 — Playwright browser management for 1C web client // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Playwright browser management for 1C web client. @@ -1446,7 +1446,7 @@ export async function fillField(name, value) { } /** Click a button/hyperlink/tab on the current form. Use {dblclick: true} to double-click (open items from lists). */ -export async function clickElement(text, { dblclick, table, toggle } = {}) { +export async function clickElement(text, { dblclick, table, toggle, expand } = {}) { ensureConnected(); await dismissPendingErrors(); if (highlightMode) try { await highlight(text, { table }); await page.waitForTimeout(500); await unhighlight(); } catch {} @@ -1562,7 +1562,7 @@ export async function clickElement(text, { dblclick, table, toggle } = {}) { return state; } if (target.kind === 'gridTreeNode') { - if (toggle) { + if (expand || toggle) { // Toggle: click the tree expand/collapse icon [tree="true"] const treeIconCoords = await page.evaluate(`(() => { const p = ${JSON.stringify(`form${formNum}_`)}; @@ -1587,10 +1587,8 @@ export async function clickElement(text, { dblclick, table, toggle } = {}) { if (treeIconCoords) { await page.mouse.click(treeIconCoords.x, treeIconCoords.y); } else { - // Fallback: select row and use +/- keys - await page.mouse.click(target.x, target.y); - await page.waitForTimeout(300); - await page.keyboard.press('NumpadAdd'); + // Fallback: dblclick on row (works for trees without clickable +/- icons) + await page.mouse.dblclick(target.x, target.y); } await waitForStable(formNum); const state = await getFormState(); @@ -1603,7 +1601,7 @@ export async function clickElement(text, { dblclick, table, toggle } = {}) { await waitForStable(formNum); const state = await getFormState(); state.clicked = { kind: 'gridTreeNode', name: target.name }; - state.hint = 'Row selected. Use { toggle: true } to expand/collapse.'; + state.hint = 'Row selected. Use { expand: true } to expand/collapse.'; return state; } if (target.kind === 'gridRow') { @@ -2188,6 +2186,29 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { // 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); + + // Check if clicked cell is a checkbox (toggle-on-click, no edit mode) + const checkboxInfo = await page.evaluate(`(() => { + const el = document.elementFromPoint(${cellCoords.x}, ${cellCoords.y}); + const cell = el?.closest('.gridBox'); + if (!cell) return null; + const chk = cell.querySelector('.checkbox'); + if (!chk) return null; + const r = chk.getBoundingClientRect(); + return { checked: chk.classList.contains('select'), x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2) }; + })()`); + 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); + return results; + } + let inEdit = false; let directEditForm = null; for (let dw = 0; dw < 4; dw++) { diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index a610c0f9..d5951f4e 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -743,7 +743,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector } const isGroup = imgBox?.querySelector('.gridListH') !== null; const isParent = imgBox?.querySelector('.gridListV') !== null; const isTreeNode = line.querySelector('.gridBoxTree') !== null; - const hasChildren = imgBox?.querySelector('[tree="true"]') !== null; + const hasChildren = line.querySelector('[tree="true"]') !== null; let kind; if (isGroup) kind = 'gridGroup'; else if (isParent) kind = 'gridParent'; From 8fca42193ad59418d3ef6b2c62f81a2cccba8e87 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Mar 2026 12:36:38 +0300 Subject: [PATCH 2/5] =?UTF-8?q?feat(web-test):=20readTable/getFormState=20?= =?UTF-8?q?=E2=80=94=20expose=20unnamed=20checkbox=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unnamed checkbox columns (no header text) now appear as "(checkbox)" in getFormState().tables[].columns and readTable().columns. Checkbox cell values return "true"/"false" instead of empty strings. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/web-test/scripts/dom.mjs | 40 ++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index d5951f4e..d352a55e 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -200,7 +200,20 @@ const READ_FORM_FN = `function readForm(p) { if (box.offsetWidth === 0) return; const textEl = box.querySelector('.gridBoxText'); const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || ''; - if (text) columns.push(text); + if (text) { + columns.push(text); + } else { + // Unnamed column — check if data cells contain checkboxes + const firstLine = body?.querySelector('.gridLine'); + if (firstLine) { + const visibleHeaders = [...headLine.children].filter(c => c.offsetWidth > 0); + const idx = visibleHeaders.indexOf(box); + const cells = [...firstLine.children].filter(c => c.offsetWidth > 0); + if (cells[idx]?.querySelector('.checkbox')) { + columns.push('(checkbox)'); + } + } + } }); } const rowCount = body ? body.querySelectorAll('.gridLine').length : 0; @@ -483,7 +496,20 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto if (box.offsetWidth === 0) return; const textEl = box.querySelector('.gridBoxText'); const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || ''; - if (!text) return; + if (!text) { + // Unnamed column — check if data cells contain checkboxes + const firstLine = body?.querySelector('.gridLine'); + if (firstLine) { + const visibleHeaders = [...headLine.children].filter(c => c.offsetWidth > 0); + const idx = visibleHeaders.indexOf(box); + const cells = [...firstLine.children].filter(c => c.offsetWidth > 0); + if (cells[idx]?.querySelector('.checkbox')) { + const r = box.getBoundingClientRect(); + columns.push({ text: '(checkbox)', x: r.x, w: r.width, right: r.x + r.width }); + } + } + return; + } const r = box.getBoundingClientRect(); columns.push({ text, x: r.x, w: r.width, right: r.x + r.width }); }); @@ -501,8 +527,14 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto [...line.children].forEach(box => { if (box.offsetWidth === 0) return; const textEl = box.querySelector('.gridBoxText'); - const val = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || ''; - if (!val) return; + const chk = box.querySelector('.checkbox'); + let val; + if (chk) { + val = chk.classList.contains('select') ? 'true' : 'false'; + } else { + val = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || ''; + if (!val) return; + } // Match cell to column by X-coordinate overlap const r = box.getBoundingClientRect(); const cx = r.x + r.width / 2; From 18a198d12baea9f0249d9f0ffd1a135f6ff85dc3 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Mar 2026 12:40:15 +0300 Subject: [PATCH 3/5] fix(web-test): fillTableRow processes remaining fields after checkbox toggle Previously fillTableRow returned immediately after toggling the first checkbox field, ignoring any remaining fields. Now it recursively processes the rest on the same row. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/web-test/scripts/browser.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index 4f66dfcf..f6240b25 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -2206,6 +2206,13 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } 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); + } return results; } From 9f5e244f68f1a3c4ea4a74919314d4195162666a Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Mar 2026 12:59:06 +0300 Subject: [PATCH 4/5] fix(web-test): fillTableRow add+checkbox targets correct row via addedRowIdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab navigation skips checkbox cells (no INPUT). After Tab fill, unfilled checkbox fields are retried via direct click. Previously the retry hit the wrong row because the selected row shifted after Tab/commit. Now we record row count before "Добавить" click and use that index for the retry, ensuring checkboxes land on the same row as the text fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/web-test/scripts/browser.mjs | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index f6240b25..2a279df0 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -2110,7 +2110,16 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } // 2. Add new row if requested + let addedRowIdx = -1; if (add) { + // Count rows before add — new row will be appended at this index + addedRowIdx = await page.evaluate(`(() => { + const grid = ${gridSelector + ? `document.querySelector(${JSON.stringify(gridSelector)})` + : `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`}; + const body = grid?.querySelector('.gridBody'); + return body ? body.querySelectorAll('.gridLine').length : 0; + })()`); await clickElement('Добавить', { table }); // Poll for edit mode (INPUT inside grid) instead of fixed 1000ms wait for (let aw = 0; aw < 6; aw++) { @@ -3036,6 +3045,45 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { } const notFilled = [...pending].filter(([_, info]) => !info.filled).map(([key]) => key); + + // Retry unfilled checkbox fields via direct click (Tab skips checkbox cells) + if (notFilled.length > 0) { + const checkboxFields = {}; + for (const key of notFilled) { + const val = String(pending.get(key).value).toLowerCase().trim(); + if (['true', 'false', 'да', 'нет', '1', '0', 'yes', 'no'].includes(val)) { + checkboxFields[key] = pending.get(key).value; + } + } + if (Object.keys(checkboxFields).length > 0) { + // Use row index: addedRowIdx (from add mode) or fallback to selected row + const currentRow = addedRowIdx >= 0 ? addedRowIdx : (row != null ? row : await page.evaluate(`(() => { + const grid = ${gridSelector + ? `document.querySelector(${JSON.stringify(gridSelector)})` + : `(() => { const grids = [...document.querySelectorAll('.grid')].filter(el => el.offsetWidth > 0); return grids[grids.length - 1]; })()`}; + if (!grid) return -1; + const body = grid.querySelector('.gridBody'); + if (!body) return -1; + const lines = [...body.querySelectorAll('.gridLine')]; + const sel = lines.findIndex(l => l.classList.contains('selected')); + return sel >= 0 ? sel : lines.length - 1; + })()`) + ); + if (currentRow >= 0) { + const more = await fillTableRow(checkboxFields, { row: currentRow, table }); + if (Array.isArray(more)) { + results.push(...more); + } else if (more?.filled) { + results.push(...more.filled); + } + for (const key of Object.keys(checkboxFields)) { + const idx = notFilled.indexOf(key); + if (idx >= 0) notFilled.splice(idx, 1); + } + } + } + } + const formData = await getFormState(); const result = { filled: results }; if (notFilled.length > 0) result.notFilled = notFilled; From 21a0e360ef20166f53aeb82a9ea0faec158419b0 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 16 Mar 2026 13:04:27 +0300 Subject: [PATCH 5/5] fix(web-test): fillTableRow stops Tab early when only checkboxes remain Tab past the last cell in 1C creates extra rows. Now when all unfilled fields are checkboxes (boolean values), the Tab loop exits immediately instead of pressing Tab 3 more times on non-INPUT cells. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/web-test/scripts/browser.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index 2a279df0..2b438b35 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -2537,7 +2537,10 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) { if (cell.tag !== 'INPUT' || !cell.fullName) { // Not in an editable grid cell — Tab past (ERP has DIV focus between cells) nonInputCount++; - if (nonInputCount > 3) break; // truly exited edit mode + // If only checkbox fields remain unfilled, stop Tab'ing to avoid creating extra rows + const onlyCheckboxLeft = [...pending.values()].every(p => p.filled || + ['true', 'false', 'да', 'нет', '1', '0', 'yes', 'no'].includes(p.value.toLowerCase().trim())); + if (nonInputCount > 3 || onlyCheckboxLeft) break; await page.keyboard.press('Tab'); await page.waitForTimeout(300); continue;