mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-18 16:49:41 +03:00
feat(web-test): readTable отдаёт состояние строки по ведущей иконке
readTable матчил только спрайты pictureCollection (именованные pic-колонки), а ведущая иконка состояния приходит из convertPicture?url=e1csys/<dir>/<file>.zip&gx=N и молча дропалась. Проверить «помечен на удаление» можно было только выводом колонки через «Настроить список» в каждом тесте. Строка списка объектов теперь отдаёт плоские булевы _deleted / _posted / _predefined / _completed / _started / _finished и сырьё _rowPic для диагностики. Ключ словаря — ПОЛНЫЙ путь спрайта, не имя файла: basic/folder.zip и accnt/folder.zip — разные файлы с одинаковым именем и разной раскладкой gx (в basic gx=1 элемент, в accnt gx=1 предопределённый). Отсутствие булева значит «не знаю», а не false: ось неприменима либо кадр не расшифрован. Дефолт false отвергнут — врал бы молча в зелёную сторону. Пути и раскладки сняты живьём на ERP; у Task/BusinessProcess раскладка сверена с данными списка. Кадры 4/5 basic/folder.zip (второе измерение — иерархия элементов) расшифрованы по байтовому равенству кадров: gx4 ≡ gx1, gx5 ≡ gx3. Стенд: документы в заданных состояниях + помеченный элемент справочника + безымянная picture-колонка ПЕРЕД значком состояния — воспроизводит ловушку «первый .gridBoxImg не тот» (проверено подменой на наивный экстрактор). Тест 22-row-state; полный регресс 27/27. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -191,6 +191,22 @@ Special row fields:
|
||||
- `hierarchical: true` — list has groups (on result object)
|
||||
- `viewMode: 'tree'` — tree view active (on result object)
|
||||
|
||||
Row state — in object lists, decoded from the row's state icon (no need to add a column to the list):
|
||||
- `_deleted: true|false` — marked for deletion (catalogs, documents, tasks, business processes, charts of accounts/calculation types)
|
||||
- `_posted: true|false` — documents
|
||||
- `_predefined: true|false` — catalogs, charts of accounts/calculation types
|
||||
- `_completed: true|false` — tasks
|
||||
- `_started`, `_finished` — business processes
|
||||
- `_rowPic: '<icon>:<N>'` — raw icon id, for diagnostics
|
||||
|
||||
```js
|
||||
const t = await readTable();
|
||||
const doc = t.rows.find(r => r['Номер'] === 'ТД00-000005');
|
||||
if (doc._deleted === true) { /* marked for deletion */ }
|
||||
```
|
||||
|
||||
**A missing state field means "unknown", never `false`** — the property may not apply (documents have no `_predefined`), or the icon may be unrecognised. So `if (!row._deleted)` is unsafe: it reads "unknown" as "not deleted". Compare explicitly (`=== true` / `=== false`) and treat `undefined` as a third outcome. Rows outside object lists (form tabular sections, value lists) have no state fields at all. If `_rowPic` is present but the booleans aren't, report its value — that icon needs decoding support.
|
||||
|
||||
**`total` is misleading for long lists.** 1С virtualizes both dynamic lists and form tabular sections — the DOM holds only a window of visible rows. `total` / `shown` count what's *loaded right now*, not the size of the underlying collection. Use **`hasMore`** to know if there's more data outside the window:
|
||||
|
||||
```js
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test dom v1.18 — facade re-exporting injectable DOM scripts from dom/
|
||||
// web-test dom v1.19 — facade re-exporting injectable DOM scripts from dom/
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Facade: re-exports DOM selector & semantic mapping script generators.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// web-test dom/grid v1.12 — grid resolution + table reading + edit-time helpers
|
||||
// web-test dom/grid v1.13 — grid resolution + table reading + edit-time helpers
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { ROW_CLICK_POINT_FN, HEADERLESS_GRID_FN } from './_shared.mjs';
|
||||
import { ROW_STATE_FN } from './row-state.mjs';
|
||||
|
||||
/**
|
||||
* Resolve a specific grid by semantic name (table parameter).
|
||||
@@ -95,6 +96,16 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
return { gx: m ? m[1] : '0' };
|
||||
}
|
||||
${HEADERLESS_GRID_FN}
|
||||
${ROW_STATE_FN}
|
||||
|
||||
// Write the leading row-state sprite onto a row: _rowPic (raw, always) + decoded booleans
|
||||
// (only when the sprite path AND frame are known — absence means "unknown", never false).
|
||||
function applyRowState(row, line) {
|
||||
const st = rowStateInfo(line);
|
||||
if (!st) return;
|
||||
row._rowPic = st.rowPic;
|
||||
if (st.axes) Object.keys(st.axes).forEach(k => { row['_' + k] = st.axes[k]; });
|
||||
}
|
||||
|
||||
// DOM-based parsing: gridHead → columns, gridBody → gridLine rows → gridBox cells
|
||||
const head = grid.querySelector('.gridHead');
|
||||
@@ -139,12 +150,13 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
}
|
||||
row[c.name] = val;
|
||||
});
|
||||
// Row meta — mirrors the headed path (group/parent/tree/level/selected)
|
||||
// Row meta — mirrors the headed path (group/parent/tree/level/selected/state)
|
||||
const imgBox = line.querySelector('.gridBoxImg');
|
||||
if (imgBox) {
|
||||
if (imgBox.querySelector('.gridListH')) row._kind = 'group';
|
||||
else if (imgBox.querySelector('.gridListV')) row._kind = 'parent';
|
||||
}
|
||||
applyRowState(row, line);
|
||||
const treeBox = line.querySelector('.gridBoxTree');
|
||||
if (treeBox) {
|
||||
const treeIcon = imgBox?.querySelector('[tree="true"]');
|
||||
@@ -331,6 +343,8 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
||||
if (imgBox.querySelector('.gridListH')) row._kind = 'group';
|
||||
else if (imgBox.querySelector('.gridListV')) row._kind = 'parent';
|
||||
}
|
||||
// Leading row-state sprite → _rowPic + decoded booleans
|
||||
applyRowState(row, line);
|
||||
// Tree mode: detect expand/collapse state and indent level
|
||||
const treeBox = line.querySelector('.gridBoxTree');
|
||||
if (treeBox) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// web-test dom/row-state v1.0 — leading row-state sprite decoding
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Ведущая иконка состояния строки списка.
|
||||
*
|
||||
* 1С рисует состояние строки (проведён / помечен на удаление / выполнена / …) отдельным спрайтом,
|
||||
* приходящим из `e1cib/convertPicture?url=e1csys/<dir>/<file>.zip&…&gx=<N>`, где `gx` — индекс кадра.
|
||||
* Это НЕ то же самое, что именованные pic-колонки (`pictureCollection/picture/<id>`) — у тех другой
|
||||
* URL и другой смысл (значение ячейки), их разбирает picInfo() в grid.mjs.
|
||||
*
|
||||
* Ключ словаря — ПОЛНЫЙ путь спрайта, не имя файла: `basic/folder.zip` (справочники) и
|
||||
* `accnt/folder.zip` (план счетов) — разные файлы с одинаковым именем и РАЗНОЙ раскладкой gx
|
||||
* (у basic gx=1 — элемент, у accnt gx=1 — предопределённый). Нормализация до имени файла склеила бы
|
||||
* их и дала неверную расшифровку.
|
||||
*
|
||||
* Набор осей свой у каждого спрайта: docList → deleted+posted, folder → deleted+predefined,
|
||||
* Task → deleted+completed. Неприменимая ось не эмитится. Незнакомый путь или нерасшифрованный gx →
|
||||
* только `_rowPic`, без булевых: ОТСУТСТВИЕ БУЛЕВА ЗНАЧИТ «НЕ ЗНАЮ», а не false.
|
||||
*
|
||||
* Раскладки сняты живьём на ERP и подтверждены отрисовкой кадров спрайта (convertPicture&scale).
|
||||
*/
|
||||
export const ROW_STATE_FN = `function rowStateInfo(line) {
|
||||
// Словарь: полный путь → (gx → набор осей). null = кадр не расшифрован (только _rowPic).
|
||||
const SPRITES = {
|
||||
// Документы, журналы документов. Спрайт — СЕТКА: база + 3·пометка_удаления.
|
||||
// База: 0 записан, 1 проведён, 2 «загнутый угол» (не расшифрован).
|
||||
// Кадров ровно 6 (gx 6..8 пустые). gx4 (проведён+помечен) на практике недостижим:
|
||||
// пометка проведённого документа его распроводит.
|
||||
'e1csys/basic/docList.zip': function (gx) {
|
||||
if (gx > 5) return null;
|
||||
const base = gx % 3, deleted = gx >= 3;
|
||||
if (base === 2) return { deleted: deleted }; // база не расшифрована → posted не эмитим
|
||||
return { deleted: deleted, posted: base === 1 };
|
||||
},
|
||||
// Справочники, планы видов характеристик. Ось группа/элемент СОЗНАТЕЛЬНО не выводим —
|
||||
// _kind живёт от .gridListH/.gridListV, на нём завязана иерархия.
|
||||
//
|
||||
// Кадры 4/5 — не «неизвестное состояние», а ВТОРОЕ измерение: у справочника с иерархией
|
||||
// элементов (ERP «Партнеры») родитель — элемент, а не папка, и платформа берёт кадр 4.
|
||||
// Кадры попарно байт-в-байт равны: gx4 ≡ gx1 (чистый элемент), gx5 ≡ gx3 (элемент + красный
|
||||
// крест), gx8 ≡ gx7. Что означает второе измерение — знать не нужно: обе НАШИ оси кадр
|
||||
// задаёт однозначно (крест есть → помечен).
|
||||
'e1csys/basic/folder.zip': {
|
||||
0: { deleted: false, predefined: false }, // группа
|
||||
1: { deleted: false, predefined: false }, // элемент
|
||||
2: { deleted: true, predefined: false }, // группа, помечена
|
||||
3: { deleted: true, predefined: false }, // элемент, помечен
|
||||
4: { deleted: false, predefined: false }, // элемент-узел иерархии (кадр ≡ gx1)
|
||||
5: { deleted: true, predefined: false }, // элемент-узел, помечен (кадр ≡ gx3)
|
||||
6: { deleted: false, predefined: true }, // группа, предопределённая
|
||||
7: { deleted: false, predefined: true }, // элемент, предопределённый
|
||||
8: { deleted: false, predefined: true }, // кадр ≡ gx7
|
||||
},
|
||||
// План счетов — ОТДЕЛЬНЫЙ спрайт с той же basename, но иной раскладкой.
|
||||
'e1csys/accnt/folder.zip': {
|
||||
0: { deleted: false, predefined: false },
|
||||
1: { deleted: false, predefined: true },
|
||||
2: { deleted: true, predefined: false },
|
||||
},
|
||||
// Задачи. Бит-поле: 1·Выполнена + 2·ПометкаУдаления. Сверено с данными на ERP.
|
||||
'e1csys/bp/Task.zip': function (gx) {
|
||||
if (gx > 3) return null;
|
||||
return { completed: (gx & 1) === 1, deleted: (gx & 2) === 2 };
|
||||
},
|
||||
// Бизнес-процессы. Бит-поле: 4·Стартован + 2·Завершён + 1·ПометкаУдаления. Сверено с данными.
|
||||
'e1csys/bp/BusinessProcess.zip': function (gx) {
|
||||
if (gx > 7) return null;
|
||||
return { started: (gx & 4) === 4, finished: (gx & 2) === 2, deleted: (gx & 1) === 1 };
|
||||
},
|
||||
// Планы видов расчёта.
|
||||
'e1csys/calc/calcKindImg.zip': {
|
||||
0: { deleted: false, predefined: false },
|
||||
1: { deleted: true, predefined: false },
|
||||
2: { deleted: false, predefined: true },
|
||||
},
|
||||
// Планы обмена. gx 1, 4..7 не сняты → только _rowPic.
|
||||
'e1csys/backend/DataExchangeImages.zip': {
|
||||
0: { thisNode: false },
|
||||
2: { thisNode: true },
|
||||
},
|
||||
};
|
||||
|
||||
function decodeUrl(s) {
|
||||
// В DOM путь закодирован ДВАЖДЫ: url=e1csys%252Fbasic%252FdocList.zip
|
||||
let v = s;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let d;
|
||||
try { d = decodeURIComponent(v); } catch (e) { break; }
|
||||
if (d === v) break;
|
||||
v = d;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Ведущих значков в строке может быть несколько в РАЗНЫХ боксах: вид операции
|
||||
// (pictureCollection, произвольная картинка конфигурации), tree-toggle и состояние.
|
||||
// Поэтому перебираем все .dIB строки, а не берём первый .gridBoxImg.
|
||||
const dibs = line.querySelectorAll('.gridBoxImg .dIB');
|
||||
for (let i = 0; i < dibs.length; i++) {
|
||||
const d = dibs[i];
|
||||
if (d.getAttribute('tree') === 'true') continue;
|
||||
const bg = d.style.backgroundImage || '';
|
||||
if (!bg.includes('convertPicture')) continue;
|
||||
const um = bg.match(/[?&]url=([^&"')]+)/);
|
||||
if (!um) continue;
|
||||
const path = decodeUrl(um[1]);
|
||||
if (!/\\.zip$/.test(path)) continue;
|
||||
const gm = bg.match(/[?&]gx=(\\d+)/);
|
||||
const gx = gm ? parseInt(gm[1], 10) : 0;
|
||||
|
||||
const entry = SPRITES[path];
|
||||
const axes = typeof entry === 'function' ? entry(gx) : (entry ? entry[gx] : null);
|
||||
return { rowPic: path + ':' + gx, axes: axes || null };
|
||||
}
|
||||
return null;
|
||||
}`;
|
||||
@@ -260,6 +260,23 @@ console.log('Расшифровка:', JSON.stringify(drilldown.rows));
|
||||
- `_selected: true` — строка выделена (подсвечена). Используйте с `clickElement({ modifier: 'ctrl'|'shift' })` для проверки мультиселекции
|
||||
- На объекте результата: `hierarchical: true`, `viewMode: 'tree'`
|
||||
|
||||
Состояние строки — в списках объектов, расшифровывается по иконке состояния (выводить колонку в список не нужно):
|
||||
|
||||
- `_deleted: true|false` — помечен на удаление (справочники, документы, задачи, бизнес-процессы, планы счетов и видов расчёта)
|
||||
- `_posted: true|false` — документы
|
||||
- `_predefined: true|false` — справочники, планы счетов и видов расчёта
|
||||
- `_completed: true|false` — задачи
|
||||
- `_started`, `_finished` — бизнес-процессы
|
||||
- `_rowPic: '<иконка>:<N>'` — сырой идентификатор иконки, для диагностики
|
||||
|
||||
```js
|
||||
const t = await readTable();
|
||||
const doc = t.rows.find(r => r['Номер'] === 'ТД00-000005');
|
||||
if (doc._deleted === true) { /* помечен на удаление */ }
|
||||
```
|
||||
|
||||
**Отсутствие поля состояния значит «не знаю», а не `false`** — свойство может быть неприменимо (у документов нет `_predefined`) либо иконка не распознана. Поэтому `if (!row._deleted)` небезопасно: «не знаю» читается как «не помечен». Сравнивайте явно (`=== true` / `=== false`), а `undefined` считайте третьим исходом. У строк вне списков объектов (табличные части форм, списки значений) полей состояния нет вовсе. Если `_rowPic` есть, а булевых нет — пришлите его значение, для этой иконки нужна поддержка расшифровки.
|
||||
|
||||
**Виртуализация и `hasMore`.** 1С виртуализирует и динамические списки, и табличные части — в DOM лежит только окно видимых строк. Поля `total` / `shown` — это размер DOM-окна, а **не** размер коллекции. Чтобы понять, есть ли строки за пределами окна, используйте `hasMore`:
|
||||
|
||||
```js
|
||||
|
||||
@@ -349,6 +349,17 @@ export const steps = [
|
||||
\t\tЭлемент.ВидНоменклатуры = Перечисления.ВидыНоменклатуры.Товар;
|
||||
\t\tЭлемент.Записать();
|
||||
\tКонецЦикла;
|
||||
\t// Элемент С ПОМЕТКОЙ УДАЛЕНИЯ — для теста ведущей иконки состояния строки (22-row-state).
|
||||
\t// Внутри группы Товары, а не на верхнем уровне: 08-hierarchy проверяет ровно 3 группы сверху.
|
||||
\tПомеченный = Справочники.Номенклатура.СоздатьЭлемент();
|
||||
\tПомеченный.Родитель = ГруппаТовары;
|
||||
\tПомеченный.Наименование = "Товар помеченный";
|
||||
\tПомеченный.Артикул = "TDEL1";
|
||||
\tПомеченный.Цена = 999;
|
||||
\tПомеченный.Активен = Истина;
|
||||
\tПомеченный.ВидНоменклатуры = Перечисления.ВидыНоменклатуры.Товар;
|
||||
\tПомеченный.Записать();
|
||||
\tПомеченный.Ссылка.ПолучитьОбъект().УстановитьПометкуУдаления(Истина);
|
||||
\tДля Сч = 1 По 10 Цикл
|
||||
\t\tЭлемент = Справочники.Номенклатура.СоздатьЭлемент();
|
||||
\t\tЭлемент.Родитель = ГруппаУслуги;
|
||||
@@ -427,6 +438,37 @@ export const steps = [
|
||||
\t\tСтрока.Сумма = Строка.Количество * Строка.Цена;
|
||||
\tКонецЦикла;
|
||||
\tДокДлинный.Записать(РежимЗаписиДокумента.Запись);
|
||||
\tЗаполнитьДокументыСостояний(Организация, Контрагенты, Номенклатура);
|
||||
КонецПроцедуры
|
||||
|
||||
// Документы в ЗАДАННЫХ состояниях — для теста ведущей иконки состояния строки (22-row-state).
|
||||
// Ищутся по Комментарий, не по позиции: список сортируется по дате, порядок не гарантирован.
|
||||
Процедура ЗаполнитьДокументыСостояний(Организация, Контрагенты, Номенклатура)
|
||||
\tСостояния = Новый Массив;
|
||||
\tСостояния.Добавить("StatePosted");
|
||||
\tСостояния.Добавить("StateDeleted");
|
||||
\tСостояния.Добавить("StateWritten");
|
||||
\tДля Каждого Состояние Из Состояния Цикл
|
||||
\t\tДок = Документы.ПриходнаяНакладная.СоздатьДокумент();
|
||||
\t\tДок.Дата = ТекущаяДата();
|
||||
\t\tДок.Организация = Организация;
|
||||
\t\tДок.Контрагент = Контрагенты[0];
|
||||
\t\tДок.Склад = "Основной";
|
||||
\t\tДок.Комментарий = Состояние;
|
||||
\t\tСтрока = Док.Товары.Добавить();
|
||||
\t\tСтрока.Номенклатура = Номенклатура[0];
|
||||
\t\tСтрока.Количество = 1;
|
||||
\t\tСтрока.Цена = 100;
|
||||
\t\tСтрока.Сумма = 100;
|
||||
\t\tЕсли Состояние = "StatePosted" Тогда
|
||||
\t\t\tДок.Записать(РежимЗаписиДокумента.Проведение);
|
||||
\t\tИначе
|
||||
\t\t\tДок.Записать(РежимЗаписиДокумента.Запись);
|
||||
\t\t\tЕсли Состояние = "StateDeleted" Тогда
|
||||
\t\t\t\tДок.Ссылка.ПолучитьОбъект().УстановитьПометкуУдаления(Истина);
|
||||
\t\t\tКонецЕсли;
|
||||
\t\tКонецЕсли;
|
||||
\tКонецЦикла;
|
||||
КонецПроцедуры
|
||||
`,
|
||||
},
|
||||
@@ -848,6 +890,14 @@ export const steps = [
|
||||
],
|
||||
elements: [
|
||||
{ table: 'Список', path: 'Список', columns: [
|
||||
// Произвольная картинка в ВЕДУЩЕЙ позиции — рядом с платформенным значком состояния
|
||||
// строки. Даёт строку с двумя значками: pictureCollection (эта колонка) и
|
||||
// convertPicture (состояние). Тест 22-row-state ловит на ней экстрактор, который
|
||||
// берёт первый .gridBoxImg вместо нужного.
|
||||
// titleLocation none — колонка БЕЗ шапки: попадает в ветку именования безымянных
|
||||
// колонок readTable (по тултипу заголовка → '(picture)'), а не в обычную.
|
||||
{ picField: 'Метка', path: 'Список.Posted', title: 'Метка', titleLocation: 'none',
|
||||
valuesPicture: 'StdPicture.Favorites' },
|
||||
{ input: 'Date', path: 'Список.Date', title: 'Дата' },
|
||||
{ input: 'Number', path: 'Список.Number', title: 'Номер' },
|
||||
{ input: 'Контрагент', path: 'Список.Контрагент', title: 'Контрагент' },
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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 нельзя.
|
||||
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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user