fix(web-test): fillTableRow enum support + clickElement tab ambiguity

Three fixes:

1. fillTableRow: match cells by column header text (headerText fallback)
   when INPUT id-based fuzzy match fails due to metadata typos

2. fillTableRow: EDD filter preserves standalone enum values like "Создать"
   by only filtering "Создать элемент/группу/:" patterns (was: startsWith)

3. clickElement: coordinate-based click for tabs without ID, avoiding
   global [data-content] selector that picks invisible duplicates from
   background forms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-17 12:39:44 +03:00
parent 1da43109fc
commit d9e7d9c107
2 changed files with 71 additions and 32 deletions
+63 -28
View File
@@ -1619,31 +1619,32 @@ export async function clickElement(text, { dblclick, table, toggle, expand } = {
return state;
}
// Build selector: tabs without ID use [data-content], others use [id]
const selector = (target.kind === 'tab' && !target.id)
? `[data-content="${target.name}"]`
: `[id="${target.id}"]`;
// Use Playwright click for proper mousedown/mouseup events
try {
await page.click(selector, { timeout: 5000 });
} catch (clickErr) {
if (clickErr.message.includes('intercepts pointer events')) {
// Surface overlay intercepts — try force click first (no side effects),
// then Escape + retry as fallback (Escape can trigger save dialogs on forms)
try {
await page.click(selector, { force: true, timeout: 5000 });
} catch (clickErr2) {
if (clickErr2.message.includes('intercepts pointer events')) {
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
await page.click(selector, { timeout: 5000 });
} else {
throw clickErr2;
// Tabs without ID use coordinate click to avoid global [data-content] ambiguity
if (target.kind === 'tab' && !target.id && target.x && target.y) {
await page.mouse.click(target.x, target.y);
} else {
const selector = `[id="${target.id}"]`;
// Use Playwright click for proper mousedown/mouseup events
try {
await page.click(selector, { timeout: 5000 });
} catch (clickErr) {
if (clickErr.message.includes('intercepts pointer events')) {
// Surface overlay intercepts — try force click first (no side effects),
// then Escape + retry as fallback (Escape can trigger save dialogs on forms)
try {
await page.click(selector, { force: true, timeout: 5000 });
} catch (clickErr2) {
if (clickErr2.message.includes('intercepts pointer events')) {
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
await page.click(selector, { timeout: 5000 });
} else {
throw clickErr2;
}
}
} else {
throw clickErr;
}
} else {
throw clickErr;
}
}
@@ -2526,10 +2527,29 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
if (!f) return { tag: 'none' };
if (f.tagName === 'INPUT' || f.tagName === 'TEXTAREA') {
const inGrid = (() => { let n = f; while (n) { if (n.classList?.contains('grid') || n.classList?.contains('gridContent')) return true; n = n.parentElement; } return false; })();
if (inGrid) return {
tag: 'INPUT', id: f.id,
fullName: f.id.replace(/^form\\d+_/, '').replace(/_i\\d+$/, '')
};
if (inGrid) {
let headerText = '';
let grid = f; while (grid && !grid.classList?.contains('grid')) grid = grid.parentElement;
if (grid) {
const fr = f.getBoundingClientRect();
const head = grid.querySelector('.gridHead');
const hl = head?.querySelector('.gridLine') || head;
if (hl) for (const h of hl.children) {
if (h.offsetWidth === 0) continue;
const hr = h.getBoundingClientRect();
if (fr.x >= hr.x && fr.x < hr.x + hr.width) {
const t = h.querySelector('.gridBoxText');
headerText = (t || h).innerText?.trim() || '';
break;
}
}
}
return {
tag: 'INPUT', id: f.id,
fullName: f.id.replace(/^form\\d+_/, '').replace(/_i\\d+$/, ''),
headerText
};
}
}
return { tag: f.tagName || 'none' };
})()`);
@@ -2578,6 +2598,19 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
}
}
// Fallback: match by column header text (handles metadata typos in cell id)
if (!matchedKey && cell.headerText) {
const htLower = cell.headerText.toLowerCase();
for (const [key, info] of pending) {
if (info.filled) continue;
const kl = key.toLowerCase();
if (htLower === kl || htLower.endsWith(kl) || htLower.includes(kl)) {
matchedKey = key;
break;
}
}
}
if (!matchedKey) {
// Skip this cell
await page.keyboard.press('Tab');
@@ -2739,7 +2772,9 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
if (eddItems && eddItems.length > 0) {
// Reference field with autocomplete — click best match
const realItems = eddItems.filter(i => !i.startsWith('Создать'));
// Filter out reference field "create" actions (Создать элемент, Создать группу, Создать: ...)
// but keep standalone enum values like "Создать" (no space/colon after)
const realItems = eddItems.filter(i => !/^Создать[\s:]/.test(i));
if (realItems.length > 0) {
const tgt = normYo(text.toLowerCase());
+8 -4
View File
@@ -686,7 +686,9 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
}
return false;
}).forEach(el => {
items.push({ id: el.id, name: el.dataset.content, label: '', kind: 'tab' });
const r = el.getBoundingClientRect();
items.push({ id: el.id, name: el.dataset.content, label: '', kind: 'tab',
x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) });
});
// Navigation panel items (FormNavigationPanel) — in parent page{N}
@@ -724,7 +726,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
if (!cf) cf = containerItems.find(i => i.label && i.label.toLowerCase() === target);
if (!cf && target.length >= 4) cf = containerItems.find(i => i.name.toLowerCase().includes(target));
if (!cf && target.length >= 4) cf = containerItems.find(i => i.label && i.label.toLowerCase().includes(target));
if (cf) return { id: cf.id, kind: cf.kind, name: cf.name };
if (cf) { const res = { id: cf.id, kind: cf.kind, name: cf.name }; if (cf.x != null) { res.x = cf.x; res.y = cf.y; } return res; }
// Fallback: filter by gridName id-prefix (e.g. ИсходящиеКоманднаяПанель_Добавить)
const gridName = gridEl.id ? gridEl.id.replace(p, '') : '';
if (gridName) {
@@ -732,7 +734,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
let pf = prefixItems.find(i => i.name.toLowerCase() === target);
if (!pf && target.length >= 4) pf = prefixItems.find(i => i.label && i.label.toLowerCase().includes(target));
if (!pf && target.length >= 4) pf = prefixItems.find(i => i.name.toLowerCase().includes(target));
if (pf) return { id: pf.id, kind: pf.kind, name: pf.name };
if (pf) { const res = { id: pf.id, kind: pf.kind, name: pf.name }; if (pf.x != null) { res.x = pf.x; res.y = pf.y; } return res; }
}
}
// Fall through to unscoped search
@@ -749,7 +751,9 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
if (!found && target.length >= 4) found = items.find(i => i.label && i.label.toLowerCase().includes(target));
if (found) {
return { id: found.id, kind: found.kind, name: found.name };
const res = { id: found.id, kind: found.kind, name: found.name };
if (found.x != null) { res.x = found.x; res.y = found.y; }
return res;
}
// Grid rows — fallback: search in table rows (for hierarchical/tree navigation)