Files
cc-1c-skills/tests/web-test/22-row-state.test.mjs
T
Nick Shirokov 7696a8b3ee fix(web-test): единая модель колонок грида — colindex первым, геометрия запасной
Резолверов колонок было пять, все независимые, и каждый ломался по-своему:
readTable (геометрия X + Y-подряды), clickElement (геометрия X, без Y),
поиск строки {кол: знач} (геометрия X, без Y и fixed-гарда), filterList
(порядковый индекс шапки), fillTableRow (colindex — единственный целый).

Механика поломки (снята живьём на списке задач ERP): шапка «Исполнитель»
широкая (x 1085..1515) и накрывает «Срок» (1085..1251) и «Выполнена»
(1251..1515). Ячейка «Исполнитель» имеет центр 1300 → приписывается к группе
«Выполнена» → та получает лишний под-ряд → срабатывает эвристика «объединённая
шапка» → фантомные «Выполнена 1/2», а значения соседей склеиваются через ' / '.

Теперь COLUMN_MODEL_FN (dom/_shared.mjs) — единственный источник правды:
buildColumnModel / columnForCell / cellForColumn / resolveColumnByName. Идентичность
колонки — colindex (собственный id колонки в 1С, есть и на шапке, и на ячейке);
геометрия работает только там, где своей шапки у ячейки нет — под-ряды
объединённой шапки («Субконто Дт» над тремя ячейками). Путь записи пришёл к этому
решению раньше (grid-edit.mjs: «reliable across merged headers») — остальные
выровнены по нему.

Следствие: имя колонки из readTable теперь годится для клика/заполнения/фильтра —
раньше readTable отдавал «Субконто Дт 2», а клик про такое имя не знал.

Попутно закрыт второй дефект: безымянная picture-колонка определялась по ПЕРВОЙ
строке, а picField над Boolean не рисует картинку при Ложь → колонка пропадала из
columns целиком. Модель сэмплит до 10 строк и ищет ячейку по colindex.

Проверено:
- 24-multirow-header (стенд, оба паттерна ERP) — зелёный; до правки красный;
- клик проверяется по факту (DOM select+focus), а не по эху clicked.column:
  до правки клик по «Срок» молча жал «Исполнитель 2» и рапортовал успех;
- живьём на ERP: список задач — 9 честных колонок вместо фантомов, значения на
  местах; форма операции (шапка 2 этажа, строка 3 под-ряда) — «Субконто Дт/Кт 1..3»
  сохранены, ничего не поехало;
- полный регресс 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:14:27 +03:00

106 lines
7.7 KiB
JavaScript

export const name = 'row-state: ведущая иконка состояния строки (_rowPic + булевы)';
export const tags = ['row-state', 'table'];
export const timeout = 90000;
export default async function({ navigateSection, openCommand, clickElement, closeForm, readTable, getPage, assert, step, log }) {
await step('документы: _posted / _deleted по заданным состояниям', async () => {
await navigateSection('Склад');
await openCommand('Приходная накладная');
const t = await readTable({ maxRows: 50 });
const byComment = (c) => t.rows.find(r => r['Комментарий'] === c);
const posted = byComment('StatePosted');
const deleted = byComment('StateDeleted');
const written = byComment('StateWritten');
log(`posted=${JSON.stringify(posted?._rowPic)} deleted=${JSON.stringify(deleted?._rowPic)} written=${JSON.stringify(written?._rowPic)}`);
assert.ok(posted && deleted && written, 'три документа-фикстуры состояний видны в списке');
// Проведён: _posted true, пометки нет
assert.equal(posted._posted, true, 'StatePosted → _posted true');
assert.equal(posted._deleted, false, 'StatePosted → _deleted false');
// Помечен на удаление. Пометка проведённого его распроводит, поэтому _posted здесь false.
assert.equal(deleted._deleted, true, 'StateDeleted → _deleted true');
// Записан, но не проведён — обе оси явный false, а не отсутствие ключа
assert.equal(written._posted, false, 'StateWritten → _posted false');
assert.equal(written._deleted, false, 'StateWritten → _deleted false');
// Сырьё указывает на платформенный спрайт документов
assert.ok(/^e1csys\/basic\/docList\.zip:\d+$/.test(posted._rowPic), `_rowPic формата docList.zip:<gx> (got ${posted._rowPic})`);
});
await step('ловушка: произвольная картинка ПЕРЕД значком состояния не подменяет его', async () => {
// Безымянная колонка «Метка» (picField над Posted) рисует pictureCollection в colindex 0,
// а платформенный значок состояния идёт ПОСЛЕ неё. Экстрактор, берущий
// line.querySelector('.gridBoxImg') вместо перебора всех .dIB, схватит картинку колонки
// и потеряет состояние. Гард ниже — через DOM: колонка безымянная и в columns не
// попадает (её наличие readTable определяет по первой строке, а там Posted=false →
// картинки нет), поэтому проверить её через публичный API нельзя.
// Колонка безымянная (titleLocation:none) → readTable называет её '(picture)'. Её наличие
// определяется сэмплированием НЕСКОЛЬКИХ строк: picField над Posted не рисует картинку при
// Ложь, а первая строка списка — непроведённый документ. По одной первой строке колонка
// выпадала из columns целиком.
const tt = await readTable({ maxRows: 50 });
assert.includes(tt.columns, '(picture)', 'безымянная picture-колонка есть в columns');
const postedRow = tt.rows.find(r => r['Комментарий'] === 'StatePosted');
assert.equal(postedRow['(picture)'], 'pic:0', 'у проведённого документа картинка нарисована');
const page = await getPage();
const icons = await page.evaluate(() => {
const grid = [...document.querySelectorAll('.grid')].find(g => g.offsetWidth > 0 && g.offsetHeight > 0);
const line = [...grid.querySelectorAll('.gridBody .gridLine')].find(l => (l.innerText || '').includes('StatePosted'));
if (!line) return null;
return [...line.querySelectorAll('.gridBoxImg .dIB')]
.map(d => d.style.backgroundImage || '')
.filter(Boolean)
.map(bg => bg.includes('convertPicture') ? 'state' : (bg.includes('pictureCollection') ? 'arbitrary' : 'other'));
});
log(`icons in row: ${JSON.stringify(icons)}`);
assert.ok(icons, 'строка StatePosted найдена в DOM');
assert.includes(icons, 'arbitrary', 'ловушка на месте: в строке есть произвольная картинка');
assert.includes(icons, 'state', 'в строке есть платформенный значок состояния');
assert.equal(icons[0], 'arbitrary', 'произвольная картинка идёт ПЕРВОЙ — первый .gridBoxImg не тот');
const t = await readTable({ maxRows: 50 });
const posted = t.rows.find(r => r['Комментарий'] === 'StatePosted');
assert.ok(posted, 'документ StatePosted виден');
assert.ok(posted._rowPic?.startsWith('e1csys/basic/docList.zip:'),
`_rowPic должен указывать на спрайт состояния, а не на картинку колонки (got ${posted._rowPic})`);
assert.equal(posted._posted, true, 'состояние разобрано несмотря на картинку перед ним');
});
await step('справочник: _deleted / _predefined на элементах Номенклатуры', async () => {
await navigateSection('Склад');
await openCommand('Номенклатура');
await clickElement('Ещё');
await clickElement('Установить стандартные настройки');
// Список показывает группы верхнего уровня — элементы прячутся внутри Товары.
await clickElement('Товары', { expand: true });
const t = await readTable({ maxRows: 50 });
const marked = t.rows.find(r => r['Наименование'] === 'Товар помеченный');
const normal = t.rows.find(r => r['Наименование'] === 'Товар 01');
log(`marked=${JSON.stringify(marked?._rowPic)} normal=${JSON.stringify(normal?._rowPic)}`);
assert.ok(marked && normal, 'помеченный и обычный товары видны');
assert.equal(marked._deleted, true, 'Товар помеченный → _deleted true');
assert.equal(normal._deleted, false, 'Товар 01 → _deleted false');
assert.equal(normal._predefined, false, 'Товар 01 → _predefined false');
assert.ok(/^e1csys\/basic\/folder\.zip:\d+$/.test(normal._rowPic), `_rowPic формата folder.zip:<gx> (got ${normal._rowPic})`);
});
await step('негатив: у табчасти формы ведущей иконки нет — полей не появляется', async () => {
await navigateSection('Склад');
await openCommand('Приходная накладная');
await clickElement('Создать');
const t = await readTable({ table: 'Товары' });
// Табчасть — не список объектов, состояния у строки нет. Отсутствие ключа значит
// «не знаю», и это правильный ответ, а не _deleted:false.
assert.ok(t.rows.every(r => r._rowPic === undefined), 'ни у одной строки нет _rowPic');
assert.ok(t.rows.every(r => r._deleted === undefined), 'ни у одной строки нет _deleted');
await closeForm();
});
}