diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 2a7cf11d..c01ee359 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -193,7 +193,7 @@ const t = await readTable(); ``` - `hasMore.below` — always present. `true` ⇒ scrolling down (PageDown / `clickElement` with `scroll:true`) will reveal more rows. -- `hasMore.above` — only present for tabular sections with a visible scrollbar widget. Dynamic lists hide their scrollbar so we cannot detect "above" reliably; treat absence as unknown. +- `hasMore.above` — usually present too. Detected via the dynamic-list page-turn buttons (#vertButtonScroll) or the tabular-section scrollbar. Absent only for rare grids that have neither widget — treat absence as unknown. ```js const t = await readTable({ maxRows: 50 }); diff --git a/.claude/skills/web-test/scripts/dom/grid.mjs b/.claude/skills/web-test/scripts/dom/grid.mjs index 38a9e22c..2b6b2786 100644 --- a/.claude/skills/web-test/scripts/dom/grid.mjs +++ b/.claude/skills/web-test/scripts/dom/grid.mjs @@ -1,4 +1,4 @@ -// web-test dom/grid v1.6 — grid resolution + table reading + edit-time helpers +// web-test dom/grid v1.7 — grid resolution + table reading + edit-time helpers // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** @@ -241,19 +241,32 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto } const isTree = !!body.querySelector('.gridBoxTree'); const hasGroups = rows.some(r => r._kind === 'group'); - // Virtualization-aware "has more" signal: - // - Tabular sections render a visible scrollbar widget (#vertScroll_* with class "scrollV" and non-zero size). - // Its child tracks expose exact above/below pixel offsets relative to the slider. - // - Dynamic lists hide the widget (empty class, 0×0). We can only infer below via scrollHeight>clientHeight. + // Virtualization-aware hasMore signal. Three sources in priority order: + // 1. Dynamic-list turn buttons (#vertButtonScroll_, sibling of grid). + // Buttons carry data-home/data-up (above) and data-down/data-end (below); + // class "disabled" on a direction means nothing to show there. + // 2. Tabular-section scrollbar (#vertScroll_, class scrollV) — + // track-back/track-next pixel heights tell us above/below precisely. + // 3. Fallback: scrollHeight>clientHeight for "below"; "above" unknown. let hasMore; - const vsId = 'vertScroll_' + (grid.id || '').replace(p, ''); - const vs = grid.querySelector('#' + CSS.escape(vsId)); - if (vs && vs.classList.contains('scrollV') && vs.offsetWidth > 0) { - const back = vs.querySelector('[data-track-back]')?.offsetHeight ?? 0; - const next = vs.querySelector('[data-track-next]')?.offsetHeight ?? 0; - hasMore = { above: back > 0, below: next > 0 }; + const turnsBox = document.getElementById('vertButtonScroll_' + grid.id); + if (turnsBox && turnsBox.offsetHeight > 0) { + const upBtns = turnsBox.querySelectorAll('[data-home], [data-up]'); + const dnBtns = turnsBox.querySelectorAll('[data-down], [data-end]'); + hasMore = { + above: [...upBtns].some(b => !b.classList.contains('disabled')), + below: [...dnBtns].some(b => !b.classList.contains('disabled')), + }; } else { - hasMore = { below: body.scrollHeight > body.clientHeight }; + const vsId = 'vertScroll_' + grid.id; + const vs = document.getElementById(vsId); + if (vs && vs.classList.contains('scrollV') && vs.offsetWidth > 0) { + const back = vs.querySelector('[data-track-back]')?.offsetHeight ?? 0; + const next = vs.querySelector('[data-track-next]')?.offsetHeight ?? 0; + hasMore = { above: back > 0, below: next > 0 }; + } else { + hasMore = { below: body.scrollHeight > body.clientHeight }; + } } const result = { name, columns: columns.map(c => c.text), rows, total, offset: ${offset}, shown: rows.length, hasMore }; if (isTree) result.viewMode = 'tree'; @@ -631,13 +644,19 @@ export function snapshotGridScript(gridSelector) { const lines = body.querySelectorAll('.gridLine'); const txt = ln => ln?.querySelector('.gridBoxText')?.innerText?.trim() || ''; const selIdx = [...lines].findIndex(l => l.classList.contains('selRow') || l.classList.contains('select')); - const vsId = 'vertScroll_' + (grid.id || '').replace(/^form\\d+_/, ''); - const vs = grid.querySelector('#' + CSS.escape(vsId)); + // hasBelow priority: (1) dynamic-list turn buttons, (2) tabular scrollbar tracks, (3) scrollHeight. let hasBelow; - if (vs && vs.classList.contains('scrollV') && vs.offsetWidth > 0) { - hasBelow = (vs.querySelector('[data-track-next]')?.offsetHeight ?? 0) > 0; + const turnsBox = document.getElementById('vertButtonScroll_' + grid.id); + if (turnsBox && turnsBox.offsetHeight > 0) { + const dnBtns = turnsBox.querySelectorAll('[data-down], [data-end]'); + hasBelow = [...dnBtns].some(b => !b.classList.contains('disabled')); } else { - hasBelow = body.scrollHeight > body.clientHeight; + const vs = document.getElementById('vertScroll_' + grid.id); + if (vs && vs.classList.contains('scrollV') && vs.offsetWidth > 0) { + hasBelow = (vs.querySelector('[data-track-next]')?.offsetHeight ?? 0) > 0; + } else { + hasBelow = body.scrollHeight > body.clientHeight; + } } return { firstText: txt(lines[0]), diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index 74cb3bf9..814616dc 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -260,37 +260,56 @@ console.log('Расшифровка:', JSON.stringify(drilldown.rows)); - `_selected: true` — строка выделена (подсвечена). Используйте с `clickElement({ modifier: 'ctrl'|'shift' })` для проверки мультиселекции - На объекте результата: `hierarchical: true`, `viewMode: 'tree'` -#### clickElement — клик по ячейке SpreadsheetDocument +**Виртуализация и `hasMore`.** 1С виртуализирует и динамические списки, и табличные части — в DOM лежит только окно видимых строк. Поля `total` / `shown` — это размер DOM-окна, а **не** размер коллекции. Чтобы понять, есть ли строки за пределами окна, используйте `hasMore`: -Для расшифровки отчётов первый аргумент `clickElement` принимает объект `{ row, column }` вместо текста. Координаты соответствуют выводу `readSpreadsheet()`: +```js +const t = await readTable(); +// t.hasMore = { above: false, below: true } — открыли список, есть строки ниже +// t.hasMore = { above: true, below: false } — пролистали в конец +// t.hasMore = { above: false, below: false } — всё помещается / нет страниц +``` + +`hasMore.below` присутствует всегда. `hasMore.above` тоже обычно есть — определяется по кнопкам пагинации (`vertButtonScroll`, есть у большинства дин-списков) или треку скроллбара (у табчастей). Отсутствует только в редких случаях, когда у грида нет ни кнопок, ни видимого скроллбара — тогда трактуйте отсутствие как «неизвестно». + +#### clickElement — клик по ячейке (spreadsheet или грид формы) + +Первый аргумент `clickElement` принимает объект `{ row, column }` вместо текста. Маршрутизация автоматическая: если на форме отрисован SpreadsheetDocument (отчёт) — кликаем туда (drill-down), иначе — по ячейке грида (табчасть, список). Параметр `table: 'ИмяГрида'` принудительно указывает грид, если на форме одновременно есть отчёт и таблицы. + +**SpreadsheetDocument (drill-down отчёта).** Координаты соответствуют выводу `readSpreadsheet()`: ```js const report = await readSpreadsheet(); // report.data[0] = { 'К1': 'Материалы строительные', 'К6': '150 000' } -// По индексу строки данных + имя колонки -await clickElement({ row: 0, column: 'К6' }, { dblclick: true }); - -// По значению ячейки в строке (fuzzy match) -await clickElement({ row: { 'К1': 'Материалы' }, column: 'К6' }, { dblclick: true }); - -// Строка итогов -await clickElement({ row: 'totals', column: 'К6' }, { dblclick: true }); +await clickElement({ row: 0, column: 'К6' }, { dblclick: true }); // по индексу +await clickElement({ row: { 'К1': 'Материалы' }, column: 'К6' }, { dblclick: true }); // по фильтру +await clickElement({ row: 'totals', column: 'К6' }, { dblclick: true }); // итоги +await clickElement('150 000', { dblclick: true }); // fallback: по тексту в iframe'ах ``` -Текстовый поиск тоже работает — если элемент не найден в основном DOM, `clickElement` ищет в SpreadsheetDocument iframe'ах: +**Грид формы (табчасть документа, список каталога/журнала).** Колонка вне viewport — авто-скролл по горизонтали (с учётом frozen-колонок). `scroll: true | number` включает reveal-loop через PageDown для filter-row за пределами DOM-окна: ```js -await clickElement('150 000', { dblclick: true }); // найдёт ячейку в отчёте +await clickElement({ row: 0, column: 'Количество' }, { table: 'Товары', dblclick: true }); +await clickElement({ row: { 'Номенклатура': 'Бумага' }, column: 'Цена' }, { table: 'Товары' }); +await clickElement( + { row: { 'Номер': '0000-000601' }, column: 'Сумма' }, + { table: 'Реализации', scroll: true } // PageDown loop, лимит по умолчанию 50 +); ``` +**Подводные камни:** +- `row: <число>` — индекс в **текущем DOM-окне**, не абсолютный (1С виртуализирует длинные списки). Для произвольной строки в длинном списке — `row: { col: val }` + `scroll: true`. +- `scroll: true` идёт только **вниз** (PageDown). Для вверх — `page.keyboard.press('Home')` через `getPage()` или сначала `filterList`. +- На дубликаты при фильтре — первая подходящая строка. Уточняйте фильтр для disambiguation. + ### Действия Все action-функции возвращают **плоский form state** (как `getFormState()`) с action-specific extras (`clicked`, `selected`, `filled`, `notFilled`, `closed`, `opened`, `navigated`, `deleted`, `filtered`, `unfiltered`). Errors всегда на верхнем уровне `.errors` — exec-wrapper автоматически throw'ает на soft validation errors (`modal`/`balloon`). | Функция | Описание | Возвращает | |---------|----------|------------| -| `clickElement(text, {dblclick?, modifier?})` | Клик по кнопке/ссылке/строке. `{dblclick: true}` для открытия, `{modifier: 'ctrl'\|'shift'}` для мультиселекции. Первый аргумент может быть `{row, column}` для клика по ячейке SpreadsheetDocument (см. выше) | form state или `{ submenu }` | +| `clickElement(text, {dblclick?, modifier?, table?, scroll?})` | Клик по кнопке/ссылке/строке. `{dblclick: true}` для открытия, `{modifier: 'ctrl'\|'shift'}` для мультиселекции. Первый аргумент может быть `{row, column}` для клика по ячейке spreadsheet или грида формы (`table` форсит грид; `scroll: true \| number` включает reveal-loop через PageDown — см. выше) | form state или `{ submenu }` | | `fillFields({name: value})` | Заполнить поля (текст, чекбокс, радио, ссылки, DCS-фильтры). Пустое значение (`''`/`null`) = очистка | form state с `filled` | | `selectValue(field, search, opts?)` | Выбрать из справочника. search: текст, `{поле: значение}` или `''`/`null` для очистки. `{ type }` для составного типа | form state с `selected` | | `fillTableRow(fields, {tab?, add?, row?})` | Заполнить строку. Значение: строка, `{ value, type }` для составного типа, `''`/`null` для очистки | form state с `filled` (per-field ошибки как items `ok: false`, см. ниже) + `notFilled?` |