mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-17 16:25:16 +03:00
fix(web-test): клик по строке грида на узких модальных формах подбора
clickElement по строке грида целился в центр `.gridLine`, который на узкой модальной форме подбора со множеством колонок (≈2775px при окне ~894px) уезжал за пределы окна → mouse.click мимо строки, выделение не менялось (clickElement при этом рапортовал успех). Проявлялось на форме множественного выбора (F4). Общая логика клика вынесена в `ROW_CLICK_POINT_FN` (_shared.mjs): первая видимая текстовая ячейка строки (skip checkbox/picture, skip tree-toggle, кламп X). Фикс применён в findClickTargetScript (forms.mjs, row-select) и переиспользован в scanGridRowsScript (grid.mjs, дедупликация inline-блока — путь selectValue). Регресс: новый tests/web-test/20-modal-select-row.test.mjs (RED до фикса, GREEN после). Полный набор — 23/23. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom shared v1.0 — embedded JS function constants
|
// web-test dom shared v1.1 — embedded JS function constants
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* Shared function strings embedded into page.evaluate() generators.
|
* Shared function strings embedded into page.evaluate() generators.
|
||||||
@@ -14,6 +14,35 @@ export const HAS_VISIBLE_MODAL_FN = `function hasVisibleModal() {
|
|||||||
return false;
|
return false;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click point INSIDE a grid row's first visible text cell — NOT the row-line centre.
|
||||||
|
*
|
||||||
|
* A wide multi-column row's centre `x = line.x + line.width/2` lands far beyond the
|
||||||
|
* form's horizontal viewport (the `.gridLine` spans ALL columns, frozen + scrollable),
|
||||||
|
* so `mouse.click` at that X falls on an overlay outside the visible grid and the row
|
||||||
|
* is never hit — the click silently does nothing. Seen on narrow modal selection forms
|
||||||
|
* with many columns (множественный выбор) and the `not_selectable` bug on selection forms.
|
||||||
|
*
|
||||||
|
* Picks the first visible non-checkbox cell that HAS text (so center-clicking never
|
||||||
|
* toggles a checkbox/picture mark), skips the first column on tree grids (it holds the
|
||||||
|
* expand toggle), and clamps X near the left edge (`min(width/2, 60)`) so a wide first
|
||||||
|
* column still lands in the viewport.
|
||||||
|
*
|
||||||
|
* @param line a `.gridLine` element
|
||||||
|
* @param body the grid's `.gridBody` (for tree detection); may be null
|
||||||
|
* @returns `{ x, y }` rounded, or `null` when the row has no usable cell.
|
||||||
|
*/
|
||||||
|
export const ROW_CLICK_POINT_FN = `function rowClickPoint(line, body) {
|
||||||
|
const isTree = !!(body && body.querySelector('.gridBoxTree'));
|
||||||
|
let cells = [...line.children]
|
||||||
|
.filter(b => b.offsetWidth > 0)
|
||||||
|
.map(b => ({ r: b.getBoundingClientRect(), checkbox: !!b.querySelector('.checkbox'), hasText: !!b.querySelector('.gridBoxText') }));
|
||||||
|
if (isTree && cells.length > 1) cells = cells.slice(1);
|
||||||
|
const pick = cells.find(c => !c.checkbox && c.hasText) || cells.find(c => !c.checkbox) || cells[0];
|
||||||
|
if (!pick) return null;
|
||||||
|
return { x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)), y: Math.round(pick.r.y + pick.r.height / 2) };
|
||||||
|
}`;
|
||||||
|
|
||||||
/** Detect active form number. Picks form with most visible elements, skipping form0.
|
/** Detect active form number. Picks form with most visible elements, skipping form0.
|
||||||
* When modalSurface is visible — prefer the highest-numbered form (modal dialog). */
|
* When modalSurface is visible — prefer the highest-numbered form (modal dialog). */
|
||||||
export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + `
|
export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + `
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// web-test dom/forms v1.6 — form detection, content read, click-target/field-button resolution
|
// web-test dom/forms v1.7 — form detection, content read, click-target/field-button resolution
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import { DETECT_FORM_FN, READ_FORM_FN } from './_shared.mjs';
|
import { DETECT_FORM_FN, READ_FORM_FN, ROW_CLICK_POINT_FN } from './_shared.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect the active form number.
|
* Detect the active form number.
|
||||||
@@ -34,6 +34,7 @@ export function readFormScript(formNum) {
|
|||||||
export function findClickTargetScript(formNum, text, { tableName, gridSelector } = {}) {
|
export function findClickTargetScript(formNum, text, { tableName, gridSelector } = {}) {
|
||||||
const p = `form${formNum}_`;
|
const p = `form${formNum}_`;
|
||||||
return `(() => {
|
return `(() => {
|
||||||
|
${ROW_CLICK_POINT_FN}
|
||||||
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/ё/gi, 'е');
|
const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/ё/gi, 'е');
|
||||||
const target = ${JSON.stringify(text.toLowerCase().replace(/ё/g, 'е'))};
|
const target = ${JSON.stringify(text.toLowerCase().replace(/ё/g, 'е'))};
|
||||||
const p = ${JSON.stringify(p)};
|
const p = ${JSON.stringify(p)};
|
||||||
@@ -196,9 +197,14 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
|||||||
else if (isParent) kind = 'gridParent';
|
else if (isParent) kind = 'gridParent';
|
||||||
else if (isTreeNode && hasChildren) kind = 'gridTreeNode';
|
else if (isTreeNode && hasChildren) kind = 'gridTreeNode';
|
||||||
else kind = 'gridRow';
|
else kind = 'gridRow';
|
||||||
|
// Click point: first visible text cell of the row, NOT the row-line centre.
|
||||||
|
// A wide multi-column row's centre lands beyond the form's viewport (e.g. on
|
||||||
|
// narrow modal selection forms) so mouse.click misses the row. See ROW_CLICK_POINT_FN.
|
||||||
|
const pt = rowClickPoint(line, body);
|
||||||
const r = line.getBoundingClientRect();
|
const r = line.getBoundingClientRect();
|
||||||
return { id: '', kind, name: rowTexts[0] || '', gridId: grid.id,
|
return { id: '', kind, name: rowTexts[0] || '', gridId: grid.id,
|
||||||
x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
x: pt ? pt.x : Math.round(r.x + r.width / 2),
|
||||||
|
y: pt ? pt.y : Math.round(r.y + r.height / 2) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// web-test dom/grid v1.10 — grid resolution + table reading + edit-time helpers
|
// web-test dom/grid v1.11 — grid resolution + table reading + edit-time helpers
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
import { ROW_CLICK_POINT_FN } from './_shared.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a specific grid by semantic name (table parameter).
|
* Resolve a specific grid by semantic name (table parameter).
|
||||||
@@ -427,6 +428,7 @@ export function getSelectedOrLastRowIndexScript(gridSelector) {
|
|||||||
*/
|
*/
|
||||||
export function scanGridRowsScript(formNum, search) {
|
export function scanGridRowsScript(formNum, search) {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
|
${ROW_CLICK_POINT_FN}
|
||||||
const p = 'form${formNum}_';
|
const p = 'form${formNum}_';
|
||||||
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
||||||
if (!grid) return null;
|
if (!grid) return null;
|
||||||
@@ -513,21 +515,18 @@ export function scanGridRowsScript(formNum, search) {
|
|||||||
|
|
||||||
if (!sel) return { rowCount: lines.length, visibleSample };
|
if (!sel) return { rowCount: lines.length, visibleSample };
|
||||||
|
|
||||||
// Click point: first visible text cell of the row (mirror findFocusCellScript)
|
// Click point: first visible text cell of the row (skip checkboxes; on tree grids
|
||||||
// — skip checkboxes; on tree grids skip the first (expand-toggle) column.
|
// skip the expand-toggle column; clamp X near the left). Shared with the
|
||||||
// Clamp X near the left so a wide first column still lands in the viewport.
|
// clickElement row-select path — see ROW_CLICK_POINT_FN.
|
||||||
const isTree = !!body.querySelector('.gridBoxTree');
|
const pt = rowClickPoint(sel, body);
|
||||||
let cells = visCells(sel).map(b => ({ r: b.getBoundingClientRect(), checkbox: !!b.querySelector('.checkbox'), hasText: !!b.querySelector('.gridBoxText') }));
|
if (!pt) return { rowCount: lines.length, visibleSample };
|
||||||
if (isTree && cells.length > 1) cells = cells.slice(1);
|
|
||||||
const pick = cells.find(c => !c.checkbox && c.hasText) || cells.find(c => !c.checkbox) || cells[0];
|
|
||||||
if (!pick) return { rowCount: lines.length, visibleSample };
|
|
||||||
|
|
||||||
const imgBox = sel.querySelector('.gridBoxImg');
|
const imgBox = sel.querySelector('.gridBoxImg');
|
||||||
const isGroup = imgBox ? !!imgBox.querySelector('.gridListH') : false;
|
const isGroup = imgBox ? !!imgBox.querySelector('.gridListH') : false;
|
||||||
return {
|
return {
|
||||||
rowCount: lines.length,
|
rowCount: lines.length,
|
||||||
x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)),
|
x: pt.x,
|
||||||
y: Math.round(pick.r.y + pick.r.height / 2),
|
y: pt.y,
|
||||||
isGroup, matchKind, visibleSample
|
isGroup, matchKind, visibleSample
|
||||||
};
|
};
|
||||||
})()`;
|
})()`;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
export const name = 'modal-select: clickElement по строке широкой модальной формы подбора (F4)';
|
||||||
|
export const tags = ['table', 'multi-select', 'modal'];
|
||||||
|
export const timeout = 90000;
|
||||||
|
|
||||||
|
// Регресс бага: на УЗКОЙ модальной форме подбора с МНОГИМ числом колонок
|
||||||
|
// (Контрагенты — 13 колонок, форма «Выбор контрагента») clickElement по строке
|
||||||
|
// не выделял строку. Причина: координата клика бралась как центр `.gridLine`,
|
||||||
|
// который шире окна (≈2775px) и уезжал за правый край модалки → mouse.click мимо.
|
||||||
|
// Фикс (ROW_CLICK_POINT_FN): клик в первую видимую текстовую ячейку строки.
|
||||||
|
//
|
||||||
|
// Отличие от 17-multiselect: тот гоняет ctrl/shift на ПОЛНОЭКРАННОМ списке, где
|
||||||
|
// центр gridLine ещё попадает во вьюпорт — баг там не воспроизводится.
|
||||||
|
//
|
||||||
|
// Открываем обработку МножественныйВыбор, фокус на поле «Контрагенты (список)»,
|
||||||
|
// F4 → модальная форма подбора Контрагентов (table 'Список').
|
||||||
|
|
||||||
|
export default async function({ navigateLink, clickElement, getPage, wait, readTable, getFormState, closeForm, assert, step, log }) {
|
||||||
|
|
||||||
|
await step('setup: открыть МножественныйВыбор + F4 на «Контрагенты (список)»', async () => {
|
||||||
|
const r = await navigateLink('Обработка.МножественныйВыбор');
|
||||||
|
assert.equal(r.activeTab, 'Множественный выбор', 'обработка открыта');
|
||||||
|
|
||||||
|
const f = await clickElement('Контрагенты (список)');
|
||||||
|
assert.equal(f.focused?.ok, true, 'поле «Контрагенты (список)» сфокусировано');
|
||||||
|
|
||||||
|
await getPage().keyboard.press('F4');
|
||||||
|
await wait(2);
|
||||||
|
|
||||||
|
const st = await getFormState();
|
||||||
|
log(`formCount=${st.formCount} modal=${st.modal} tables=${JSON.stringify(st.tables?.map(t => t.name))}`);
|
||||||
|
assert.ok(st.modal === true, 'открыта модальная форма подбора');
|
||||||
|
assert.ok(st.tables?.some(t => t.name === 'Список'), 'таблица «Список» присутствует');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('single-select: clickElement по строке выделяет её (ядро бага)', async () => {
|
||||||
|
await clickElement('ООО Восток', { table: 'Список' });
|
||||||
|
const t = await readTable({ table: 'Список' });
|
||||||
|
const target = t.rows.find(r => r['Наименование'] === 'ООО Восток');
|
||||||
|
log(`selected: ${JSON.stringify(t.rows.filter(r => r._selected).map(r => r['Наименование']))}`);
|
||||||
|
assert.ok(target, 'строка «ООО Восток» найдена');
|
||||||
|
assert.equal(target._selected, true, 'строка выделилась кликом на модальной форме');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('ctrl-add: ctrl-click добавляет вторую строку к выделению', async () => {
|
||||||
|
await clickElement('ООО Юг', { table: 'Список', modifier: 'ctrl' });
|
||||||
|
const t = await readTable({ table: 'Список' });
|
||||||
|
const selected = t.rows.filter(r => r._selected).map(r => r['Наименование']);
|
||||||
|
log(`selected after ctrl: ${JSON.stringify(selected)}`);
|
||||||
|
assert.ok(selected.length >= 2, `≥2 строк выделено ctrl-кликом на модалке, выделено ${selected.length}`);
|
||||||
|
assert.includes(selected, 'ООО Восток', 'ООО Восток остался выделен');
|
||||||
|
assert.includes(selected, 'ООО Юг', 'ООО Юг добавлен в выделение');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('cleanup: закрыть форму подбора и обработку', async () => {
|
||||||
|
await closeForm(); // модальная форма подбора (Escape)
|
||||||
|
await closeForm(); // обработка
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user