mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-26 06:54:38 +03:00
fix(web-test): клик в видимую ячейку строки формы выбора + пер-колоночный/exact матчинг
scanGridRowsScript целился в центр всей .gridLine; на широкой форме выбора (колонок
больше, чем влезает в окно) центр-X уезжает за вьюпорт в оверлей → клик мимо строки →
Enter не выбирает → форма не закрывается → ложное not_selectable «group/folder row».
- dom/grid v1.10: матч по ячейкам (строка: exact→startsWith→includes; объект {col:val}:
пер-колоночно, AND, предпочтение exact-all), точка клика — первая видимая текстовая
ячейка строки (не центр); возврат visibleSample/isGroup. scrollIntoView не нужен
(динсписок держит в DOM только видимые строки).
- forms/select-value v1.25: структурированный search в скан (без склейки значений
объекта), различение still_open vs реальная группа (isGroup), actionable not_found
со списком видимых кандидатов.
- tests: широкая (14 колонок, choiceMode) форма выбора Контрагентов + контрагент «Север»
рядом с «ООО Север» → детерминированный регресс центр-X + exact-preference;
selectValue (04-selectvalue) и fillTableRow составной ячейки (05-table).
Проверено: живой E2E на реальной форме выбора составного типа, red-green на синтетике,
полный регресс web-test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// web-test dom/grid v1.9 — grid resolution + table reading + edit-time helpers
|
||||
// web-test dom/grid v1.10 — grid resolution + table reading + edit-time helpers
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
/**
|
||||
@@ -405,14 +405,27 @@ export function getSelectedOrLastRowIndexScript(gridSelector) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a form's grid for a row matching `searchLower` (case- and ё-insensitive,
|
||||
* NBSP-normalised). Match order: exact → startsWith → includes.
|
||||
* Scan a selection-form grid for the row matching `search` and return a click
|
||||
* point INSIDE that row's first visible text cell — NOT the row-line centre.
|
||||
* (A wide multi-column row's centre `x = r.x + r.width/2` lands beyond the form's
|
||||
* horizontal viewport, on an overlay, so `mouse.click` misses the row → Enter
|
||||
* doesn't select → form stays open. That was the `not_selectable` bug.)
|
||||
*
|
||||
* When `searchLower` is empty, returns coords of the first row (fallback).
|
||||
* `search` is either:
|
||||
* - a string — matched per-cell (case/ё/NBSP-insensitive), preferring
|
||||
* exact-cell → startsWith → includes (so "Кабель" wins over "Кабель ВВГ");
|
||||
* - an object `{ column: value, ... }` — each key fuzzy-resolved to a header
|
||||
* column, a row matches when EVERY column's cell includes its value (AND),
|
||||
* preferring rows where every column's cell equals its value exactly.
|
||||
* Empty `search` → first row (fallback).
|
||||
*
|
||||
* Returns `{ rowCount, x, y, isGroup } | { rowCount: 0 } | null`.
|
||||
* Returns:
|
||||
* `{ rowCount, x, y, isGroup, matchKind, visibleSample }` when found,
|
||||
* `{ rowCount, visibleSample, error? }` when rows present but unmatched,
|
||||
* `{ rowCount: 0 }` for an empty grid, or `null` when no grid.
|
||||
* `visibleSample` = first-cell text of visible rows, for actionable error messages.
|
||||
*/
|
||||
export function scanGridRowsScript(formNum, searchLower) {
|
||||
export function scanGridRowsScript(formNum, search) {
|
||||
return `(() => {
|
||||
const p = 'form${formNum}_';
|
||||
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
||||
@@ -421,22 +434,102 @@ export function scanGridRowsScript(formNum, searchLower) {
|
||||
if (!body) return null;
|
||||
const lines = [...body.querySelectorAll('.gridLine')];
|
||||
if (!lines.length) return { rowCount: 0 };
|
||||
const searchLower = ${JSON.stringify(searchLower || '')};
|
||||
let sel = null;
|
||||
if (searchLower) {
|
||||
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim().toLowerCase().replace(/ё/gi, 'е');
|
||||
const rowData = lines.map(l => ({ el: l, text: norm(l.innerText) }));
|
||||
sel = rowData.find(r => r.text === searchLower)?.el
|
||||
|| rowData.find(r => r.text.startsWith(searchLower))?.el
|
||||
|| rowData.find(r => r.text.includes(searchLower))?.el;
|
||||
|
||||
const search = ${JSON.stringify(search ?? '')};
|
||||
const isObj = search && typeof search === 'object';
|
||||
const norm = s => (s || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim().toLowerCase().replace(/ё/gi, 'е');
|
||||
const disp = s => (s || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim();
|
||||
const cellText = b => (b.querySelector('.gridBoxText') ? b.querySelector('.gridBoxText').innerText : b.innerText) || '';
|
||||
const visCells = line => [...line.children].filter(b => b.offsetWidth > 0);
|
||||
const visibleSample = lines.slice(0, 10)
|
||||
.map(l => disp(l.querySelector('.gridBoxText') ? l.querySelector('.gridBoxText').innerText : ''))
|
||||
.filter(Boolean);
|
||||
|
||||
let sel = null, matchKind = null;
|
||||
|
||||
if (!search || (isObj && !Object.keys(search).length)) {
|
||||
sel = lines[0]; matchKind = 'first';
|
||||
} else if (isObj) {
|
||||
// Resolve each key to a header column (fuzzy, normalised) — mirror resolveCol.
|
||||
const headLine = grid.querySelector('.gridHead .gridLine') || grid.querySelector('.gridHead');
|
||||
const headers = [...(headLine ? headLine.children : [])]
|
||||
.filter(c => c.offsetWidth > 0)
|
||||
.map(c => {
|
||||
const t = (c.querySelector('.gridBoxText') || c).innerText || '';
|
||||
const title = c.getAttribute('title') || '';
|
||||
const r = c.getBoundingClientRect();
|
||||
return { name: disp(t) || disp(title), text: t, title, x: r.x, right: r.x + r.width };
|
||||
})
|
||||
.filter(h => h.name);
|
||||
const resolveCol = name => {
|
||||
const n = norm(name);
|
||||
const cand = h => [h.text, h.title].filter(Boolean);
|
||||
return headers.find(h => cand(h).some(t => norm(t) === n))
|
||||
|| headers.find(h => cand(h).some(t => norm(t).includes(n)));
|
||||
};
|
||||
const cellAtCol = (line, col) => visCells(line).find(b => {
|
||||
const r = b.getBoundingClientRect();
|
||||
const cx = r.x + r.width / 2;
|
||||
return cx >= col.x && cx < col.right;
|
||||
});
|
||||
const keys = Object.keys(search);
|
||||
const cols = {};
|
||||
for (const k of keys) {
|
||||
const c = resolveCol(k);
|
||||
if (!c) return { rowCount: lines.length, error: 'filter_column_not_found', column: k, visibleSample };
|
||||
cols[k] = c;
|
||||
}
|
||||
let bestRank = 0;
|
||||
for (const line of lines) {
|
||||
let allIncludes = true, allExact = true;
|
||||
for (const k of keys) {
|
||||
const v = norm(search[k]);
|
||||
if (!v) continue;
|
||||
const cell = cellAtCol(line, cols[k]);
|
||||
const t = norm(cell ? cellText(cell) : '');
|
||||
if (!t.includes(v)) { allIncludes = false; break; }
|
||||
if (t !== v) allExact = false;
|
||||
}
|
||||
if (!allIncludes) continue;
|
||||
const rank = allExact ? 2 : 1;
|
||||
if (rank > bestRank) { bestRank = rank; sel = line; matchKind = allExact ? 'object-exact' : 'object'; if (rank === 2) break; }
|
||||
}
|
||||
} else {
|
||||
sel = lines[0]; // empty search → first row
|
||||
// String: per-cell, prefer exact-cell → startsWith → includes.
|
||||
const v = norm(search);
|
||||
let bestRank = 0;
|
||||
for (const line of lines) {
|
||||
let rowRank = 0;
|
||||
for (const b of visCells(line)) {
|
||||
const t = norm(cellText(b));
|
||||
if (!t) continue;
|
||||
let r = 0;
|
||||
if (t === v) r = 3; else if (t.startsWith(v)) r = 2; else if (t.includes(v)) r = 1;
|
||||
if (r > rowRank) rowRank = r;
|
||||
}
|
||||
if (rowRank > bestRank) { bestRank = rowRank; sel = line; matchKind = rowRank === 3 ? 'exact' : rowRank === 2 ? 'startsWith' : 'includes'; if (rowRank === 3) break; }
|
||||
}
|
||||
}
|
||||
if (!sel) return null;
|
||||
|
||||
if (!sel) return { rowCount: lines.length, visibleSample };
|
||||
|
||||
// Click point: first visible text cell of the row (mirror findFocusCellScript)
|
||||
// — skip checkboxes; on tree grids skip the first (expand-toggle) column.
|
||||
// Clamp X near the left so a wide first column still lands in the viewport.
|
||||
const isTree = !!body.querySelector('.gridBoxTree');
|
||||
let cells = visCells(sel).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 { rowCount: lines.length, visibleSample };
|
||||
|
||||
const imgBox = sel.querySelector('.gridBoxImg');
|
||||
const isGroup = imgBox ? !!imgBox.querySelector('.gridListH') : false;
|
||||
const r = sel.getBoundingClientRect();
|
||||
return { rowCount: lines.length, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), isGroup };
|
||||
return {
|
||||
rowCount: lines.length,
|
||||
x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)),
|
||||
y: Math.round(pick.r.y + pick.r.height / 2),
|
||||
isGroup, matchKind, visibleSample
|
||||
};
|
||||
})()`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test forms/select-value v1.24 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
|
||||
// web-test forms/select-value v1.25 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -25,12 +25,13 @@ import { getFormState } from './state.mjs';
|
||||
import { filterList } from '../table/filter.mjs';
|
||||
|
||||
/**
|
||||
* Scan visible grid rows for a text match (exact → startsWith → includes).
|
||||
* Returns center coords of the matched row, or null if not found.
|
||||
* When searchLower is empty, returns coords of the first row (fallback).
|
||||
* Scan a selection-form grid for the row matching `search` (string, or a
|
||||
* { column: value } object for per-column matching) and return a click point
|
||||
* inside the matched row's first visible text cell. See scanGridRowsScript for
|
||||
* matching rules and the return shape (`{ x, y, isGroup, visibleSample, ... }`).
|
||||
*/
|
||||
async function scanGridRows(formNum, searchLower) {
|
||||
return page.evaluate(scanGridRowsScript(formNum, searchLower));
|
||||
async function scanGridRows(formNum, search) {
|
||||
return page.evaluate(scanGridRowsScript(formNum, search));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,30 +141,44 @@ async function advancedSearchInline(formNum, text) {
|
||||
* @returns {{ field, ok, method }} or {{ field, error, message }}
|
||||
*/
|
||||
export async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum) {
|
||||
const isObj = !!search && typeof search === 'object';
|
||||
// searchText (joined for objects) is only for the paste-based search steps
|
||||
// (advancedSearchInline / simple search). Row matching uses the structured
|
||||
// `search` via scanGridRows — no lossy join there.
|
||||
const searchText = typeof search === 'string'
|
||||
? search : (search ? Object.values(search).join(' ') : '');
|
||||
? search : (isObj ? Object.values(search).join(' ') : '');
|
||||
const searchLower = normYo((searchText || '').toLowerCase());
|
||||
const hasSearch = isObj ? Object.keys(search).length > 0 : !!searchLower;
|
||||
|
||||
// Helper: try to select a row; returns result if ok, null if item wasn't selectable (group).
|
||||
// Helper: try to select a row; returns result if ok, null if it couldn't be
|
||||
// selected (real group row, or the click missed). Remembers why for the
|
||||
// final error message.
|
||||
let hadUnselectableMatch = false;
|
||||
let lastIsGroup = false;
|
||||
let lastSample = null;
|
||||
async function trySelect(row) {
|
||||
const r = await dblclickAndVerify(row, selFormNum, fieldName);
|
||||
if (r.ok) return r;
|
||||
hadUnselectableMatch = true; // found match but couldn't select (possibly group row or overlay)
|
||||
hadUnselectableMatch = true; // matched but form stayed open (group row or missed click)
|
||||
lastIsGroup = !!row.isGroup;
|
||||
return null; // form still open, try next step
|
||||
}
|
||||
// Run scanGridRows, remember the visible-row sample for actionable errors.
|
||||
async function scanAndTry(searchArg) {
|
||||
const row = await scanGridRows(selFormNum, searchArg);
|
||||
if (row?.visibleSample) lastSample = row.visibleSample;
|
||||
if (row?.x) return trySelect(row);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 1: Scan visible rows (no filtering)
|
||||
if (searchLower) {
|
||||
const row = await scanGridRows(selFormNum, searchLower);
|
||||
if (row?.x) {
|
||||
const r = await trySelect(row);
|
||||
if (r) return r;
|
||||
}
|
||||
if (hasSearch) {
|
||||
const r = await scanAndTry(search);
|
||||
if (r) return r;
|
||||
}
|
||||
|
||||
// Step 2: Advanced search (Alt+F — fast, no overlay issues)
|
||||
if (typeof search === 'object' && search) {
|
||||
if (isObj) {
|
||||
// Per-field advanced search via filterList(val, {field})
|
||||
for (const [fld, val] of Object.entries(search)) {
|
||||
try {
|
||||
@@ -180,12 +195,9 @@ export async function pickFromSelectionForm(selFormNum, fieldName, search, origF
|
||||
// Inline advanced search (Alt+F, "по части строки")
|
||||
await advancedSearchInline(selFormNum, searchText);
|
||||
}
|
||||
if (searchLower) {
|
||||
const row = await scanGridRows(selFormNum, searchLower);
|
||||
if (row?.x) {
|
||||
const r = await trySelect(row);
|
||||
if (r) return r;
|
||||
}
|
||||
if (hasSearch) {
|
||||
const r = await scanAndTry(search);
|
||||
if (r) return r;
|
||||
}
|
||||
|
||||
// Step 3: Fallback — simple search via search input (for forms without Alt+F support)
|
||||
@@ -201,32 +213,33 @@ export async function pickFromSelectionForm(selFormNum, fieldName, search, origF
|
||||
await page.keyboard.press('Enter');
|
||||
await waitForStable(selFormNum);
|
||||
} catch { /* proceed */ }
|
||||
const row = await scanGridRows(selFormNum, searchLower);
|
||||
if (row?.x) {
|
||||
const r = await trySelect(row);
|
||||
if (r) return r;
|
||||
}
|
||||
const r = await scanAndTry(search);
|
||||
if (r) return r;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Empty search → pick first row; otherwise not found
|
||||
if (!searchLower) {
|
||||
const row = await scanGridRows(selFormNum, '');
|
||||
if (row?.x) {
|
||||
const r = await trySelect(row);
|
||||
if (r) return r;
|
||||
}
|
||||
if (!hasSearch) {
|
||||
const r = await scanAndTry('');
|
||||
if (r) return r;
|
||||
}
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await waitForStable();
|
||||
const searchDesc = typeof search === 'string' ? '"' + search + '"' : JSON.stringify(search);
|
||||
const candidates = lastSample && lastSample.length ? ' Visible rows: ' + lastSample.join(', ') + '.' : '';
|
||||
if (hadUnselectableMatch) {
|
||||
if (lastIsGroup) {
|
||||
return { field: fieldName, error: 'not_selectable',
|
||||
message: 'Found ' + searchDesc + ' in selection form but it is a non-selectable group/folder row' };
|
||||
}
|
||||
// Matched a row but the selection click didn't take — the value isn't in the
|
||||
// visible result. Tell the caller to refine rather than blame a "group".
|
||||
return { field: fieldName, error: 'not_selectable',
|
||||
message: 'Found ' + searchDesc + ' in selection form but it is not selectable (group/folder row)' };
|
||||
message: 'Matched ' + searchDesc + ' but the row could not be selected (not in the visible result — refine the search).' + candidates };
|
||||
}
|
||||
return { field: fieldName, error: 'not_found',
|
||||
message: 'No matches in selection form for ' + searchDesc };
|
||||
message: 'No matches in selection form for ' + searchDesc + '.' + candidates };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,16 @@ export const steps = [
|
||||
{ name: 'Телефон', type: 'String', length: 20 },
|
||||
{ name: 'Адрес', type: 'String', length: 200 },
|
||||
{ name: 'КодКПП', type: 'String', length: 9 },
|
||||
// Доп. строковые реквизиты — выводятся в широкую ФОРМУ ВЫБОРА (ниже),
|
||||
// чтобы строка формы выбора стала шире окна выбора. Регресс бага
|
||||
// «центр широкой строки уезжает за вьюпорт → клик мимо» (04-selectvalue).
|
||||
{ name: 'Регион', type: 'String', length: 50 },
|
||||
{ name: 'Город', type: 'String', length: 50 },
|
||||
{ name: 'Улица', type: 'String', length: 100 },
|
||||
{ name: 'БИК', type: 'String', length: 9 },
|
||||
{ name: 'ОГРН', type: 'String', length: 13 },
|
||||
{ name: 'ОКПО', type: 'String', length: 10 },
|
||||
{ name: 'ВидДеятельности', type: 'String', length: 100 },
|
||||
],
|
||||
},
|
||||
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
|
||||
@@ -311,6 +321,11 @@ export const steps = [
|
||||
\tСписок.Добавить(Новый Структура("Имя,ИНН", "ООО Юг", "7700000002"));
|
||||
\tСписок.Добавить(Новый Структура("Имя,ИНН", "ООО Восток", "7700000003"));
|
||||
\tСписок.Добавить(Новый Структура("Имя,ИНН", "АО Запад", "7700000004"));
|
||||
\t// Контрагент с именем ровно «Север» рядом с «ООО Север» — для детерминированного
|
||||
\t// регресса широкой формы выбора (04-selectvalue): поиск «Север» даёт 2 вхождения,
|
||||
\t// «ООО Север» сортируется раньше. Багованный клик-по-центру/эскалация выберут
|
||||
\t// «ООО Север»; фикс через exact-preference обязан выбрать точное «Север».
|
||||
\tСписок.Добавить(Новый Структура("Имя,ИНН", "Север", "7700000005"));
|
||||
\tДля Каждого Запись Из Список Цикл
|
||||
\t\tЭлемент = Справочники.Контрагенты.СоздатьЭлемент();
|
||||
\t\tЭлемент.Наименование = Запись.Имя;
|
||||
@@ -572,6 +587,50 @@ export const steps = [
|
||||
validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'Catalogs/Контрагенты/Forms/ФормаСписка/Ext/Form.xml' },
|
||||
},
|
||||
|
||||
// Форма ВЫБОРА Контрагенты — НАМЕРЕННО ШИРОКАЯ (14 колонок), чтобы строка была
|
||||
// шире окна выбора. Регресс бага «центр широкой строки уезжает за вьюпорт →
|
||||
// клик в оверлей → not_selectable» (04-selectvalue/direct-form, выбор «Север»).
|
||||
// form-add с Purpose=Choice авто-назначает её DefaultChoiceForm → именно она
|
||||
// открывается при выборе ссылки на Контрагента.
|
||||
{
|
||||
name: 'form-add: Форма выбора Контрагенты',
|
||||
script: 'form-add/scripts/form-add',
|
||||
args: { '-ObjectPath': '{workDir}/Catalogs/Контрагенты.xml', '-FormName': 'ФормаВыбора', '-Purpose': 'Choice' },
|
||||
},
|
||||
{
|
||||
name: 'form-compile: Форма выбора Контрагенты (широкая)',
|
||||
script: 'form-compile/scripts/form-compile',
|
||||
input: {
|
||||
title: 'Выбор контрагента',
|
||||
attributes: [
|
||||
{ name: 'Список', type: 'DynamicList', main: true,
|
||||
settings: { mainTable: 'Catalog.Контрагенты', dynamicDataRead: true } },
|
||||
],
|
||||
elements: [
|
||||
// choiceMode: true → <ChoiceMode>true</ChoiceMode> на таблице: Enter/двойной
|
||||
// клик ПОДТВЕРЖДАЮТ выбор (а не открывают элемент). Без него форма ведёт
|
||||
// себя как обычный список (Enter открывает элемент).
|
||||
{ table: 'Список', path: 'Список', choiceMode: true, columns: [
|
||||
{ input: 'Code', path: 'Список.Code', title: 'Код' },
|
||||
{ input: 'Description', path: 'Список.Description', title: 'Наименование' },
|
||||
{ input: 'ИНН', path: 'Список.ИНН', title: 'ИНН' },
|
||||
{ input: 'Телефон', path: 'Список.Телефон', title: 'Телефон' },
|
||||
{ input: 'Адрес', path: 'Список.Адрес', title: 'Адрес' },
|
||||
{ input: 'КодКПП', path: 'Список.КодКПП', title: 'КПП' },
|
||||
{ input: 'Регион', path: 'Список.Регион', title: 'Регион' },
|
||||
{ input: 'Город', path: 'Список.Город', title: 'Город' },
|
||||
{ input: 'Улица', path: 'Список.Улица', title: 'Улица' },
|
||||
{ input: 'БИК', path: 'Список.БИК', title: 'БИК' },
|
||||
{ input: 'ОГРН', path: 'Список.ОГРН', title: 'ОГРН' },
|
||||
{ input: 'ОКПО', path: 'Список.ОКПО', title: 'ОКПО' },
|
||||
{ input: 'ВидДеятельности', path: 'Список.ВидДеятельности', title: 'Вид деятельности' },
|
||||
]},
|
||||
],
|
||||
},
|
||||
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/Catalogs/Контрагенты/Forms/ФормаВыбора/Ext/Form.xml' },
|
||||
validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'Catalogs/Контрагенты/Forms/ФормаВыбора/Ext/Form.xml' },
|
||||
},
|
||||
|
||||
// Форма элемента Номенклатура — 2 вкладки, все типы полей
|
||||
{
|
||||
name: 'form-add: Форма элемента Номенклатура',
|
||||
|
||||
@@ -22,18 +22,26 @@ export default async function({ navigateSection, openCommand, clickElement, sele
|
||||
await closeForm({ save: false });
|
||||
});
|
||||
|
||||
await step('direct-form: Контрагент → CatalogRef.Контрагенты (quickChoice=false)', async () => {
|
||||
await step('direct-form: Контрагент → форма выбора (ШИРОКАЯ — регресс центр-X + exact-preference)', async () => {
|
||||
// Форма выбора Контрагентов намеренно широкая (14 колонок) — строка шире окна.
|
||||
// Старый scanGridRows целился в ЦЕНТР строки → клик в оверлей за вьюпортом →
|
||||
// не та строка / not_selectable. Новый — в первую видимую ячейку.
|
||||
// В справочнике есть и «ООО Север», и ровно «Север»; поиск «Север» даёт 2
|
||||
// вхождения, «ООО Север» сортируется раньше. Багованный путь выбрал бы «ООО
|
||||
// Север»; фикс (exact-preference + клик в видимую ячейку) обязан выбрать «Север».
|
||||
await navigateSection('Склад');
|
||||
await openCommand('Приходная накладная');
|
||||
await clickElement('Создать');
|
||||
|
||||
const result = await selectValue('Контрагент', 'Север');
|
||||
log(`method=${result.selected?.method}, search=${result.selected?.search}`);
|
||||
log(`method=${result.selected?.method}, search=${result.selected?.search}, err=${result.selected?.error || ''}`);
|
||||
assert.equal(result.selected?.method, 'form', 'Должен быть метод form (через форму выбора)');
|
||||
assert.ok(!result.selected?.error, `выбор без ошибки (было not_selectable): ${result.selected?.message || ''}`);
|
||||
|
||||
const field = findField(result, 'Контрагент');
|
||||
log(`Контрагент value='${field?.value}'`);
|
||||
assert.includes(field?.value || '', 'Север', 'Контрагент должен показать выбранное значение');
|
||||
assert.equal((field?.value || '').trim(), 'Север',
|
||||
'exact-preference + клик в видимую ячейку: выбран точный «Север», не «ООО Север»');
|
||||
|
||||
await closeForm({ save: false });
|
||||
});
|
||||
|
||||
@@ -120,4 +120,30 @@ export default async function({ navigateSection, openCommand, clickElement, fill
|
||||
assert.equal(t.rows[0]['Номенклатура'], 'Товар 03', 'Удалена первая (Товар 02), осталась Товар 03');
|
||||
await closeForm({ save: false });
|
||||
});
|
||||
|
||||
await step('composite-wide-form: ИсточникТЧ {value,type} через ШИРОКУЮ форму выбора', async () => {
|
||||
// Прямой регресс исходного симптома: fillTableRow в составную ячейку → диалог
|
||||
// типа → широкая форма выбора Контрагентов (14 колонок, строка шире окна).
|
||||
// Старый scanGridRows целился в центр строки → клик в оверлей → not_selectable.
|
||||
// Детерминированность: «ООО Север» сортируется раньше точного «Север»; багованный
|
||||
// путь выбрал бы «ООО Север», фикс (exact-preference + клик в видимую ячейку) — «Север».
|
||||
await navigateSection('Склад');
|
||||
await openCommand('Приходная накладная');
|
||||
await clickElement('Создать');
|
||||
|
||||
const r = await fillTableRow(
|
||||
{ 'Источник': { value: 'Север', type: 'Контрагент' } },
|
||||
{ table: 'Товары', add: true }
|
||||
);
|
||||
log(`composite fill: ${JSON.stringify(r.filled)}`);
|
||||
const item = (r.filled || []).find(f => /сточник/.test(f.field || ''));
|
||||
assert.ok(item?.ok, `ячейка Источник заполнена без ошибки: ${item?.error || ''} ${item?.message || ''}`);
|
||||
|
||||
const t = await readTable({ table: 'Товары' });
|
||||
log(`Источник cell='${t.rows[0]?.['Источник']}'`);
|
||||
assert.equal(t.rows[0]?.['Источник'], 'Север',
|
||||
'exact-preference + клик в видимую ячейку: выбран точный «Север», не «ООО Север»');
|
||||
|
||||
await closeForm({ save: false });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user