fix(web-test): deleteTableRow выходит из cell edit-mode перед Delete

Delete-клавиша в режиме редактирования ячейки очищает буфер ввода,
а не удаляет строку. Это становилось проблемой когда:
1. предыдущий fillTableRow закончил Tab-навигацией в input (например
   в Number-ячейку соседней колонки), и фокус остался там;
2. сам click на Number/Date ячейку в deleteTableRow автоматически
   входит в edit-mode (поведение 1С).

Фикс: в deleteTableRow проверяем isInputFocusedInGrid дважды — до и
после click — и шлём Escape если активен INPUT в целевом гриде. Строка
остаётся выделенной после Escape, Delete срабатывает.

Дополнительно: isInputFocusedInGridScript / isInputFocusedInGrid теперь
принимают опциональный gridSelector — чтобы можно было прицельно проверять
конкретный грид на многогрид-формах (а не любой `.grid` на странице).

Покрытие: новый шаг в 05-table проверяет сценарий «фокус снаружи грида
(Комментарий), потом delete» — гарантирует что post-click Escape ловит
автоматический вход в edit-mode при клике на Number-ячейку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-28 21:42:10 +03:00
parent e05c0a4a61
commit 8f2fa21814
4 changed files with 49 additions and 7 deletions
@@ -1,4 +1,4 @@
// web-test dom/edit-state v1.0 — focus and popup detection inside the 1C web client
// web-test dom/edit-state v1.1 — focus and popup detection inside the 1C web client
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
/**
@@ -21,12 +21,22 @@ export function isInputFocusedScript({ allowTextarea = false } = {}) {
/**
* Is the currently focused INPUT/TEXTAREA inside a `.grid` ancestor?
* Used to verify grid edit-mode (active cell editor).
*
* @param {string} [gridSelector] — when given, only `true` if the focused input
* is inside that specific grid. Without it — any `.grid` ancestor counts.
*
* Returns boolean.
*/
export function isInputFocusedInGridScript() {
export function isInputFocusedInGridScript(gridSelector) {
const sel = gridSelector ? JSON.stringify(gridSelector) : 'null';
return `(() => {
const f = document.activeElement;
if (!f || (f.tagName !== 'INPUT' && f.tagName !== 'TEXTAREA')) return false;
const sel = ${sel};
if (sel) {
const grid = document.querySelector(sel);
return !!(grid && grid.contains(f));
}
let n = f;
while (n) {
if (n.classList?.contains('grid')) return true;
@@ -1,4 +1,4 @@
// web-test core/helpers v1.20 — private, cross-cutting helpers used by the
// web-test core/helpers v1.21 — private, cross-cutting helpers used by the
// public action functions (clickElement/fillFields/selectValue/etc).
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
@@ -94,10 +94,11 @@ export async function isInputFocused({ allowTextarea = false } = {}) {
/**
* Thin wrapper: is the currently focused INPUT/TEXTAREA inside a `.grid`?
* Used to verify grid edit-mode.
* Used to verify grid edit-mode. Pass `{ gridSelector }` to scope the check
* to a specific grid (when a form has multiple grids).
*/
export async function isInputFocusedInGrid() {
return page.evaluate(isInputFocusedInGridScript());
export async function isInputFocusedInGrid({ gridSelector } = {}) {
return page.evaluate(isInputFocusedInGridScript(gridSelector));
}
/**
@@ -1,4 +1,4 @@
// web-test table/grid v1.19 — Form-grid operations: read table rows, fill rows, delete rows.
// web-test table/grid v1.20 — Form-grid operations: read table rows, fill rows, delete rows.
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
//
// "Grid" в терминах 1С — таблица на форме (.gridLine/.gridBody/.grid в DOM):
@@ -8,6 +8,7 @@
import { page, ensureConnected } from '../core/state.mjs';
import { detectFormScript, readTableScript, resolveGridScript } from '../../dom.mjs';
import { findDeleteRowCoordsScript, countGridRowsScript } from '../../dom/grid.mjs';
import { isInputFocusedInGrid } from '../core/helpers.mjs';
import { dismissPendingErrors } from '../core/errors.mjs';
import { waitForStable } from '../core/wait.mjs';
import { clickElement } from '../core/click.mjs';
@@ -63,10 +64,26 @@ export async function deleteTableRow(row, { tab, table } = {}) {
const rowsBefore = cellCoords.total;
// Pre-click Escape: leftover edit-mode from a prior fillTableRow Tab-navigation.
// Without it the next mouse click may not select the row reliably (the active
// edit input intercepts the event timing).
if (await isInputFocusedInGrid({ gridSelector })) {
await page.keyboard.press('Escape');
await page.waitForTimeout(150);
}
// Single click to select the row
await page.mouse.click(cellCoords.x, cellCoords.y);
await page.waitForTimeout(300);
// Post-click Escape: clicking a Number/Date cell auto-enters edit mode in 1С.
// Delete in edit mode clears the cell buffer instead of deleting the row, so
// we exit edit first. The row remains selected after Escape — Delete acts on it.
if (await isInputFocusedInGrid({ gridSelector })) {
await page.keyboard.press('Escape');
await page.waitForTimeout(150);
}
// 3. Press Delete to remove the row
await page.keyboard.press('Delete');
await waitForStable();
+14
View File
@@ -90,6 +90,20 @@ export default async function({ navigateSection, openCommand, clickElement, fill
log(`rows after delete: ${t.rows?.length}, [0]=${t.rows[0]?.['Номенклатура']}`);
assert.equal(t.rows?.length, 1, 'Должна остаться 1 строка');
assert.equal(t.rows[0]['Номенклатура'], 'Товар 02', 'Осталась строка Товар 02');
});
await step('delete: фокус вне грида (Комментарий) — delete всё равно должен работать', async () => {
// Воспроизводит сценарий, когда последнее действие было НЕ в табчасти.
// deleteTableRow должен корректно перехватить фокус и удалить строку
// несмотря на то, что click на Number-ячейку входит в edit-mode (post-click Escape).
await fillTableRow({ 'Номенклатура': 'Товар 03', 'Количество': '8' }, { table: 'Товары', add: true });
// Перевести фокус на Комментарий (вне грида).
await fillFields({ 'Комментарий': 'focus-outside-grid' });
await deleteTableRow(0, { table: 'Товары' });
const t = await readTable({ table: 'Товары' });
log(`rows after delete: ${t.rows?.length}, names=${t.rows.map(r => r['Номенклатура']).join(',')}`);
assert.equal(t.rows?.length, 1, 'Должна остаться 1 строка');
assert.equal(t.rows[0]['Номенклатура'], 'Товар 03', 'Удалена первая (Товар 02), осталась Товар 03');
await closeForm({ save: false });
});
}