mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-28 07:31:00 +03:00
fix(web-test): фактическое состояние свёрнутой группы и попадание клика по заголовку
Группа, содержимое которой — таблица, отдавала collapsed:false в обоих состояниях,
а свернуть её обратно было нельзя. Два независимых дефекта:
1. Первым сиблингом за <base>#title_div платформа кладёт пустую служебную обёртку
<дочерний>#group_div.logicGroupContainer (display:block, height:0), а контент идёт
дальше по сиблингам — внутри обёртки его нет. Чтение display первого сиблинга
давало collapsed:false и свёрнутой, и развёрнутой группе.
2. <base>#title_text растягивается по ширине содержимого (173px свёрнута → 1295px
развёрнута), кликабелен только вложенный label шириной по тексту. Клик в
геометрический центр попадал в пустоту: раскрыть удавалось, свернуть — нет.
GROUP_STATE_FN обходит сиблинги по id-префиксу обёртки (префикс обрывает обход на
чужих узлах — свободный элемент между группами по-прежнему не путает определение).
TEXT_CLICK_POINT_FN целится во вложенный текст с клампом влево, как rowClickPoint.
Обе — общие для getFormState().groups[] и резолвера цели клика.
Клик по заголовку, не изменивший состояние, теперь бросает ошибку вместо
toggled:true — идемпотентность {expand} не затронута, при нечитаемом collapsed
проверка пропускается.
Фикстура: свёрнутая группа с таблицей (прежние содержали только декорации, поэтому
регресс был зелёным) и растянутая гиперссылка с Сообщить в обработчике — у декораций
промаха нет, их внутренний узел тянется вместе с контейнером.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5472e03417
commit
ba27f2218f
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom shared v1.7 — embedded JS function constants
|
// web-test dom shared v1.8 — 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.
|
||||||
@@ -43,6 +43,74 @@ export const ROW_CLICK_POINT_FN = `function rowClickPoint(line, body) {
|
|||||||
return { x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)), y: Math.round(pick.r.y + pick.r.height / 2) };
|
return { x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)), y: Math.round(pick.r.y + pick.r.height / 2) };
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click point inside a stretched text container (group title, hyperlink decoration) —
|
||||||
|
* NOT the container's centre.
|
||||||
|
*
|
||||||
|
* `<base>#title_text` is a flex box; the clickable thing is the nested
|
||||||
|
* `<label class="ellipsis" for="<groupId>">` sized by the text and pinned left. The box
|
||||||
|
* stretches to the width of the group's content, so a group holding a wide table gets a
|
||||||
|
* title 1295px wide around a 166px label: the geometric centre lands on empty space and
|
||||||
|
* the click silently does nothing (measured on the stand — collapsed title 173px, expanded
|
||||||
|
* 1295px, which is why the FIRST toggle worked and every later one did not).
|
||||||
|
*
|
||||||
|
* Same clamp as rowClickPoint: aim near the left edge so a wide box still lands on text.
|
||||||
|
*
|
||||||
|
* @param el container element (`.staticTextHyper` / title text)
|
||||||
|
* @returns `{ x, y }` rounded.
|
||||||
|
*/
|
||||||
|
export const TEXT_CLICK_POINT_FN = `function textClickPoint(el) {
|
||||||
|
const inner = el.firstElementChild;
|
||||||
|
const r = (inner && inner.offsetWidth > 0 ? inner : el).getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + Math.min(r.width / 2, 60)), y: Math.round(r.y + r.height / 2) };
|
||||||
|
}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapsed state of a form group — single source of truth for getFormState().groups[]
|
||||||
|
* and the click-target resolver.
|
||||||
|
*
|
||||||
|
* PopUp groups: the panel `<base>#panel_div` carries the state directly.
|
||||||
|
*
|
||||||
|
* Collapsible groups: 1C lays the form out FLAT (content is not nested in the group, it
|
||||||
|
* follows as siblings of `<base>#title_div`, absolutely positioned). Usually the first
|
||||||
|
* sibling is the group's own first child, and its `display` IS the state.
|
||||||
|
*
|
||||||
|
* But when that child is itself a container (a table, a nested group), 1C inserts a
|
||||||
|
* bookkeeping wrapper `<childName>#group_div.logicGroupContainer` FIRST — always
|
||||||
|
* `display:block`, always `height:0`, and EMPTY: the real nodes
|
||||||
|
* (`<childName>КоманднаяПанель_div`, `<childName>_div`, …) follow it as further siblings,
|
||||||
|
* they are not inside it. Reading that wrapper's display reported `collapsed:false` for a
|
||||||
|
* group that was genuinely collapsed, so `{expand:true}` became a silent no-op.
|
||||||
|
*
|
||||||
|
* So: on hitting the wrapper, walk the following siblings while they share its id prefix
|
||||||
|
* (the prefix stops the walk at foreign nodes such as `separatePanelArea`, keeping the
|
||||||
|
* "free element between groups" case safe) and report collapsed when none of them is
|
||||||
|
* visible. Neither the wrapper's own geometry nor the group's `<base>_div` can serve as
|
||||||
|
* the signal — both are always zero-height.
|
||||||
|
*
|
||||||
|
* @param base element id prefix without suffix, e.g. `form1_ГруппаТовары`
|
||||||
|
* @returns `true` collapsed, `false` expanded, `null` when the layout is unrecognised.
|
||||||
|
*/
|
||||||
|
export const GROUP_STATE_FN = `function groupCollapsed(base) {
|
||||||
|
const panelDiv = document.getElementById(base + '#panel_div');
|
||||||
|
if (panelDiv) return getComputedStyle(panelDiv).display === 'none';
|
||||||
|
const titleDiv = document.getElementById(base + '#title_div');
|
||||||
|
const sib = titleDiv && titleDiv.nextElementSibling;
|
||||||
|
if (!sib) return null;
|
||||||
|
const WRAP = '#group_div';
|
||||||
|
if (sib.id && sib.id.endsWith(WRAP) && sib.classList.contains('logicGroupContainer')) {
|
||||||
|
const pfx = sib.id.slice(0, -WRAP.length);
|
||||||
|
let seen = false;
|
||||||
|
for (let n = sib.nextElementSibling; n; n = n.nextElementSibling) {
|
||||||
|
if (!n.id || !n.id.startsWith(pfx)) break;
|
||||||
|
seen = true;
|
||||||
|
if (n.offsetWidth > 0 && n.offsetHeight > 0) return false;
|
||||||
|
}
|
||||||
|
return seen ? true : null;
|
||||||
|
}
|
||||||
|
return getComputedStyle(sib).display === 'none';
|
||||||
|
}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single source of truth for column derivation on HEADERLESS grids (no `.gridHead`).
|
* Single source of truth for column derivation on HEADERLESS grids (no `.gridHead`).
|
||||||
* 1C still puts `colindex` on body cells, so anchoring works without a header.
|
* 1C still puts `colindex` on body cells, so anchoring works without a header.
|
||||||
@@ -365,7 +433,7 @@ function detectForms() {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
/** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */
|
/** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */
|
||||||
export const READ_FORM_FN = HEADERLESS_GRID_FN + `
|
export const READ_FORM_FN = HEADERLESS_GRID_FN + GROUP_STATE_FN + `
|
||||||
function readForm(p) {
|
function readForm(p) {
|
||||||
const result = {};
|
const result = {};
|
||||||
const fields = [];
|
const fields = [];
|
||||||
@@ -680,11 +748,7 @@ function readForm(p) {
|
|||||||
// • рядом кнопка-каретка <base>#titleBtn (ControlRepresentation=Picture);
|
// • рядом кнопка-каретка <base>#titleBtn (ControlRepresentation=Picture);
|
||||||
// • есть панель <base>#panel_div — это ВСПЛЫВАЮЩАЯ (popup) группа.
|
// • есть панель <base>#panel_div — это ВСПЛЫВАЮЩАЯ (popup) группа.
|
||||||
// Обычные (несворачиваемые) группы не имеют ничего из этого — их не показываем.
|
// Обычные (несворачиваемые) группы не имеют ничего из этого — их не показываем.
|
||||||
// Состояние:
|
// Состояние — groupCollapsed (GROUP_STATE_FN), общая с резолвером цели клика.
|
||||||
// • popup: display панели <base>#panel_div (none → закрыта);
|
|
||||||
// • collapsible: DOM у 1С плоский (контент — сиблинги под mainGroup, не вложены), но при
|
|
||||||
// глубинном обходе Form.xml ПЕРВЫЙ контент-сиблинг сразу за #title_div — всегда дочерний
|
|
||||||
// элемент группы (свободные соседи идут после всех детей). Его display = состояние.
|
|
||||||
const groups = [];
|
const groups = [];
|
||||||
document.querySelectorAll('[id^="' + p + '"][id$="#title_text"]').forEach(tt => {
|
document.querySelectorAll('[id^="' + p + '"][id$="#title_text"]').forEach(tt => {
|
||||||
if (tt.offsetWidth === 0 && tt.offsetHeight === 0) return;
|
if (tt.offsetWidth === 0 && tt.offsetHeight === 0) return;
|
||||||
@@ -694,13 +758,9 @@ function readForm(p) {
|
|||||||
const hasBtn = !!document.getElementById(base + '#titleBtn');
|
const hasBtn = !!document.getElementById(base + '#titleBtn');
|
||||||
if (!isHyper && !hasBtn && !panelDiv) return; // обычная (несворачиваемая) группа
|
if (!isHyper && !hasBtn && !panelDiv) return; // обычная (несворачиваемая) группа
|
||||||
const g = { name: base.replace(p, ''), title: nbsp(tt.innerText?.trim() || '') };
|
const g = { name: base.replace(p, ''), title: nbsp(tt.innerText?.trim() || '') };
|
||||||
if (panelDiv) {
|
if (panelDiv) g.behavior = 'popup';
|
||||||
g.behavior = 'popup';
|
const collapsed = groupCollapsed(base);
|
||||||
g.collapsed = getComputedStyle(panelDiv).display === 'none';
|
if (collapsed !== null) g.collapsed = collapsed;
|
||||||
} else {
|
|
||||||
const contentSib = document.getElementById(base + '#title_div')?.nextElementSibling;
|
|
||||||
if (contentSib) g.collapsed = getComputedStyle(contentSib).display === 'none';
|
|
||||||
}
|
|
||||||
groups.push(g);
|
groups.push(g);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// web-test dom/forms v1.12 — form detection, content read, click-target/field-button resolution
|
// web-test dom/forms v1.13 — 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, ROW_CLICK_POINT_FN } from './_shared.mjs';
|
import { DETECT_FORM_FN, READ_FORM_FN, ROW_CLICK_POINT_FN, TEXT_CLICK_POINT_FN, GROUP_STATE_FN } from './_shared.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect the active form number.
|
* Detect the active form number.
|
||||||
@@ -73,6 +73,8 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
|||||||
const p = `form${formNum}_`;
|
const p = `form${formNum}_`;
|
||||||
return `(() => {
|
return `(() => {
|
||||||
${ROW_CLICK_POINT_FN}
|
${ROW_CLICK_POINT_FN}
|
||||||
|
${TEXT_CLICK_POINT_FN}
|
||||||
|
${GROUP_STATE_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)};
|
||||||
@@ -107,9 +109,10 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
|||||||
// Сворачиваемые/всплывающие группы — заголовок как цель раскрытия/сворачивания.
|
// Сворачиваемые/всплывающие группы — заголовок как цель раскрытия/сворачивания.
|
||||||
// Идентификация: <base>#title_text и один из: гиперссылка (TitleHyperlink) ЛИБО кнопка-каретка
|
// Идентификация: <base>#title_text и один из: гиперссылка (TitleHyperlink) ЛИБО кнопка-каретка
|
||||||
// <base>#titleBtn (Picture) ЛИБО панель <base>#panel_div (popup). Обычные группы пропускаем.
|
// <base>#titleBtn (Picture) ЛИБО панель <base>#panel_div (popup). Обычные группы пропускаем.
|
||||||
// Мишень клика: #titleBtn (вариант «картинка») иначе заголовок (у popup клик по заголовку
|
// Мишень клика: #titleBtn (вариант «картинка», компактный — бьём в центр) иначе текст
|
||||||
// и открывает, и закрывает). Состояние: у popup — display панели, иначе — первый контент-
|
// заголовка через textClickPoint: контейнер растягивается по ширине содержимого группы,
|
||||||
// сиблинг за #title_div (display:none = свёрнута/закрыта).
|
// кликабелен только вложенный label (у popup клик по заголовку и открывает, и закрывает).
|
||||||
|
// Состояние — groupCollapsed, общая с getFormState().groups[].
|
||||||
[...document.querySelectorAll('[id^="' + p + '"][id$="#title_text"]')]
|
[...document.querySelectorAll('[id^="' + p + '"][id$="#title_text"]')]
|
||||||
.filter(el => el.offsetWidth > 0 || el.offsetHeight > 0).forEach(el => {
|
.filter(el => el.offsetWidth > 0 || el.offsetHeight > 0).forEach(el => {
|
||||||
const base = el.id.slice(0, -('#title_text'.length));
|
const base = el.id.slice(0, -('#title_text'.length));
|
||||||
@@ -117,12 +120,17 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
|||||||
const btnVisible = btn && (btn.offsetWidth > 0 || btn.offsetHeight > 0);
|
const btnVisible = btn && (btn.offsetWidth > 0 || btn.offsetHeight > 0);
|
||||||
const panelDiv = document.getElementById(base + '#panel_div');
|
const panelDiv = document.getElementById(base + '#panel_div');
|
||||||
if (!el.classList.contains('staticTextHyper') && !btnVisible && !panelDiv) return; // обычная группа
|
if (!el.classList.contains('staticTextHyper') && !btnVisible && !panelDiv) return; // обычная группа
|
||||||
const tgt = btnVisible ? btn : el;
|
let pt;
|
||||||
const r = tgt.getBoundingClientRect();
|
if (btnVisible) {
|
||||||
|
const r = btn.getBoundingClientRect();
|
||||||
|
pt = { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
} else {
|
||||||
|
pt = textClickPoint(el);
|
||||||
|
}
|
||||||
const item = { id: '', kind: 'formGroup', name: norm(el.innerText) || base.replace(p, ''),
|
const item = { id: '', kind: 'formGroup', name: norm(el.innerText) || base.replace(p, ''),
|
||||||
label: base.replace(p, ''), x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
label: base.replace(p, ''), x: pt.x, y: pt.y };
|
||||||
const stateEl = panelDiv || (document.getElementById(base + '#title_div') || {}).nextElementSibling;
|
const collapsed = groupCollapsed(base);
|
||||||
if (stateEl) item.collapsed = getComputedStyle(stateEl).display === 'none';
|
if (collapsed !== null) item.collapsed = collapsed;
|
||||||
items.push(item);
|
items.push(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,6 +243,9 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
|||||||
if (found) {
|
if (found) {
|
||||||
const res = { id: found.id, kind: found.kind, name: found.name };
|
const res = { id: found.id, kind: found.kind, name: found.name };
|
||||||
if (found.disabled) res.disabled = true;
|
if (found.disabled) res.disabled = true;
|
||||||
|
// label группы = её техническое имя: по нему обработчик клика сверяет состояние
|
||||||
|
// в groups[] после клика (name там техническое, а found.name — текст заголовка).
|
||||||
|
if (found.kind === 'formGroup') res.label = found.label;
|
||||||
if (found.collapsed != null) res.collapsed = found.collapsed;
|
if (found.collapsed != null) res.collapsed = found.collapsed;
|
||||||
if (found.x != null) { res.x = found.x; res.y = found.y; }
|
if (found.x != null) { res.x = found.x; res.y = found.y; }
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// web-test forms/click-group v1.0 — click handler for collapsible/popup form-group titles.
|
// web-test forms/click-group v1.1 — click handler for collapsible/popup form-group titles.
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
//
|
//
|
||||||
// Reuses the tree/grid expand vocabulary so the model has ONE mental model:
|
// Reuses the tree/grid expand vocabulary so the model has ONE mental model:
|
||||||
@@ -20,10 +20,22 @@ export async function clickFormGroupTarget(target, ctx) {
|
|||||||
const shouldClick = shouldClickToggle(state, expand, toggle);
|
const shouldClick = shouldClickToggle(state, expand, toggle);
|
||||||
if (shouldClick) await modifierClick(target.x, target.y, modifier);
|
if (shouldClick) await modifierClick(target.x, target.y, modifier);
|
||||||
await waitForStable(formNum);
|
await waitForStable(formNum);
|
||||||
return returnFormState({
|
const result = await returnFormState({
|
||||||
clicked: { kind: 'formGroup', name: target.name, toggled: shouldClick, ...(modifier ? { modifier } : {}) },
|
clicked: { kind: 'formGroup', name: target.name, toggled: shouldClick, ...(modifier ? { modifier } : {}) },
|
||||||
hint: shouldClick
|
hint: shouldClick
|
||||||
? 'Group toggled. Call getFormState — its groups[].collapsed and revealed/hidden content update.'
|
? 'Group toggled. Call getFormState — its groups[].collapsed and revealed/hidden content update.'
|
||||||
: 'Group already in desired state.',
|
: 'Group already in desired state.',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Постусловие: клик, который ничего не переключил, — молчаливая ложь (то же правило, что
|
||||||
|
// guard на disabled в core/click.mjs). Проверяем ТОЛЬКО когда клик реально был сделан и
|
||||||
|
// состояние читаемо: при collapsed == null (нераспознанная вёрстка) сверять не с чем.
|
||||||
|
if (shouldClick && target.collapsed != null) {
|
||||||
|
const after = (result.groups || []).find(g => g.name === target.label);
|
||||||
|
if (after && after.collapsed === target.collapsed) {
|
||||||
|
throw new Error(`clickElement: group "${target.name}" did not toggle `
|
||||||
|
+ `(collapsed stayed ${after.collapsed})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1284,12 +1284,23 @@ export const steps = [
|
|||||||
title: 'Страница настроек',
|
title: 'Страница настроек',
|
||||||
// Без командной панели → на форме нет ни одного a.press.
|
// Без командной панели → на форме нет ни одного a.press.
|
||||||
properties: { commandBarLocation: 'None' },
|
properties: { commandBarLocation: 'None' },
|
||||||
|
events: { OnCreateAtServer: 'ПриСозданииНаСервере' },
|
||||||
attributes: [
|
attributes: [
|
||||||
{ name: 'Объект', type: 'DataProcessorObject.СтраницаНастроек', main: true },
|
{ name: 'Объект', type: 'DataProcessorObject.СтраницаНастроек', main: true },
|
||||||
|
// Содержимое свёрнутой группы-с-таблицей (ниже). Заполняется в ПриСозданииНаСервере.
|
||||||
|
{ name: 'ТаблицаВСвёрнутой', type: 'ValueTable', columns: [
|
||||||
|
{ name: 'Товар', type: 'CatalogRef.Номенклатура', title: 'Товар' },
|
||||||
|
{ name: 'Количество', type: 'Number(15,3)', title: 'Количество' },
|
||||||
|
]},
|
||||||
],
|
],
|
||||||
elements: [
|
elements: [
|
||||||
// Гиперссылка-декорация → .staticTextHyper (кликабельна через findClickTargetScript).
|
// Гиперссылка-декорация → .staticTextHyper (кликабельна через findClickTargetScript).
|
||||||
{ label: 'СсылкаТехническаяИнформация', title: 'Техническая информация', hyperlink: true },
|
{ label: 'СсылкаТехническаяИнформация', title: 'Техническая информация', hyperlink: true },
|
||||||
|
// РАСТЯНУТАЯ гиперссылка (horizontalStretch + autoMaxWidth:false → контейнер во всю
|
||||||
|
// ширину формы). Обработчик Нажатие сообщает — без следа доставку клика не отличить
|
||||||
|
// от no-op (у обычной декорации обработчика нет вовсе).
|
||||||
|
{ label: 'СсылкаРастянутая', title: 'Растянутая ссылка', hyperlink: true,
|
||||||
|
horizontalStretch: true, autoMaxWidth: false, events: { Click: 'СсылкаРастянутаяНажатие' } },
|
||||||
// Сворачиваемая группа с дочерней декорацией (пустая группа не рендерится).
|
// Сворачиваемая группа с дочерней декорацией (пустая группа не рендерится).
|
||||||
{ group: 'collapsible', name: 'ГруппаКлассификаторы', title: 'Классификаторы и курсы валют',
|
{ group: 'collapsible', name: 'ГруппаКлассификаторы', title: 'Классификаторы и курсы валют',
|
||||||
collapsed: true, showTitle: true, children: [
|
collapsed: true, showTitle: true, children: [
|
||||||
@@ -1334,11 +1345,46 @@ export const steps = [
|
|||||||
{ label: 'ВнутриСтрессРазвёрнутой', title: 'Внутри развёрнутой стресс-группы' },
|
{ label: 'ВнутриСтрессРазвёрнутой', title: 'Внутри развёрнутой стресс-группы' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// СВЁРНУТАЯ ГРУППА С ТАБЛИЦЕЙ — все группы выше содержат только декорации, а 1С
|
||||||
|
// рендерит их скрытыми (display:none), из-за чего эвристика «первый сиблинг за
|
||||||
|
// #title_div» работает. Грид — тяжёлый контент, платформа может создавать его
|
||||||
|
// DOM лениво (только при первом раскрытии). Тогда сиблинг за заголовком — чужой
|
||||||
|
// видимый узел → collapsed:false при реально свёрнутой группе (багрепорт 2026-07-23).
|
||||||
|
{ group: 'collapsible', name: 'ГруппаСвёрнутаяСТаблицей', title: 'Свёрнутая с таблицей',
|
||||||
|
collapsed: true, showTitle: true, children: [
|
||||||
|
{ table: 'ТаблицаВСвёрнутой', path: 'ТаблицаВСвёрнутой', changeRowSet: true, columns: [
|
||||||
|
{ input: 'Товар', path: 'ТаблицаВСвёрнутой.Товар', title: 'Товар' },
|
||||||
|
{ input: 'Количество', path: 'ТаблицаВСвёрнутой.Количество', title: 'Количество' },
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' },
|
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' },
|
||||||
validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' },
|
validate: { script: 'form-validate/scripts/form-validate', flag: '-FormPath', path: 'DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'writeFile: СтраницаНастроек form Module.bsl',
|
||||||
|
writeFile: 'DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form/Module.bsl',
|
||||||
|
content: `&НаСервере
|
||||||
|
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
|
||||||
|
\tЗапрос = Новый Запрос("ВЫБРАТЬ ПЕРВЫЕ 3 Ссылка КАК Ссылка ИЗ Справочник.Номенклатура ГДЕ НЕ ЭтоГруппа УПОРЯДОЧИТЬ ПО Наименование");
|
||||||
|
\tВыборка = Запрос.Выполнить().Выбрать();
|
||||||
|
\tСч = 0;
|
||||||
|
\tПока Выборка.Следующий() Цикл
|
||||||
|
\t\tСтрока = ТаблицаВСвёрнутой.Добавить();
|
||||||
|
\t\tСтрока.Товар = Выборка.Ссылка;
|
||||||
|
\t\tСтрока.Количество = (Сч + 1) * 10;
|
||||||
|
\t\tСч = Сч + 1;
|
||||||
|
\tКонецЦикла;
|
||||||
|
КонецПроцедуры
|
||||||
|
|
||||||
|
&НаКлиенте
|
||||||
|
Процедура СсылкаРастянутаяНажатие(Элемент)
|
||||||
|
\tСообщить("клик по растянутой ссылке доставлен");
|
||||||
|
КонецПроцедуры
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
// Обработка МножественныйВыбор — основная форма с 4 полями типа «список значений».
|
// Обработка МножественныйВыбор — основная форма с 4 полями типа «список значений».
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,6 +71,52 @@ export default async function({ navigateSection, openCommand, navigateLink, getF
|
|||||||
await closeForm();
|
await closeForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await step('свёрнутая группа С ТАБЛИЦЕЙ: collapsed против факта видимости грида', async () => {
|
||||||
|
// Багрепорт 2026-07-23. Два дефекта на этой группе (у остальных содержимое — декорации):
|
||||||
|
// 1) 1С кладёт первым сиблингом за #title_div служебную обёртку <дочерний>#group_div
|
||||||
|
// (logicGroupContainer, display:block, height:0), а контент идёт дальше по сиблингам →
|
||||||
|
// чтение display первого сиблинга давало collapsed:false в ОБОИХ состояниях;
|
||||||
|
// 2) #title_text растягивается по ширине содержимого (173px свёрнута → 1295px развёрнута),
|
||||||
|
// кликабелен только вложенный label слева → клик в центр промахивался, и свернуть
|
||||||
|
// обратно было нельзя.
|
||||||
|
// Грид в tables[] — независимая от collapsed проверка факта.
|
||||||
|
const grid = (st) => (st.tables || []).some(t => t.name === 'ТаблицаВСвёрнутой');
|
||||||
|
const s = await navigateLink('Обработка.СтраницаНастроек');
|
||||||
|
assert.equal(collapsedOf(s, 'ГруппаСвёрнутаяСТаблицей'), true, 'шаг 0: collapsed:true сразу после открытия');
|
||||||
|
assert.equal(grid(s), false, 'шаг 0: грида нет — группа действительно свёрнута');
|
||||||
|
|
||||||
|
let r = await clickElement('Свёрнутая с таблицей', { expand: true });
|
||||||
|
assert.equal(r.clicked.toggled, true, 'expand:true — кликнул');
|
||||||
|
assert.equal(collapsedOf(r, 'ГруппаСвёрнутаяСТаблицей'), false, 'раскрыта');
|
||||||
|
assert.equal(grid(r), true, 'грид появился в tables[]');
|
||||||
|
|
||||||
|
r = await clickElement('Свёрнутая с таблицей', { expand: true });
|
||||||
|
assert.equal(r.clicked.toggled, false, 'expand:true повторно — идемпотентно (no-op)');
|
||||||
|
|
||||||
|
// Свернуть обратно: до фикса точки клика этот шаг молча не срабатывал (заголовок растянут).
|
||||||
|
r = await clickElement('Свёрнутая с таблицей', { expand: false });
|
||||||
|
assert.equal(r.clicked.toggled, true, 'expand:false — кликнул');
|
||||||
|
assert.equal(collapsedOf(r, 'ГруппаСвёрнутаяСТаблицей'), true, 'свёрнута обратно');
|
||||||
|
assert.equal(grid(r), false, 'грид пропал из tables[]');
|
||||||
|
await closeForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('растянутая гиперссылка: клик доходит до обработчика', async () => {
|
||||||
|
// Проверка соседнего с дефектом 2 случая. Ссылка растянута на всю ширину формы
|
||||||
|
// (horizontalStretch + autoMaxWidth:false → контейнер 1295px), но промаха тут НЕ бывает:
|
||||||
|
// её внутренний текстовый узел — div.ellipsis.flex-1-1-100, он тянется ВМЕСТЕ с
|
||||||
|
// контейнером (1293 из 1295), тогда как у заголовка группы внутри label без flex-1-1-100,
|
||||||
|
// шириной по тексту. Шаг стоит страховкой на случай смены вёрстки платформой.
|
||||||
|
// Обработчик Нажатие сообщает — без следа доставку клика не отличить от no-op.
|
||||||
|
const сообщения = (st) => (st.errors?.messages || []).join(' | ');
|
||||||
|
const s = await navigateLink('Обработка.СтраницаНастроек');
|
||||||
|
assert.equal(сообщения(s), '', 'до клика сообщений нет');
|
||||||
|
const r = await clickElement('Растянутая ссылка');
|
||||||
|
assert.includes(сообщения(r), 'клик по растянутой ссылке доставлен',
|
||||||
|
'клик по растянутой ссылке дошёл до обработчика');
|
||||||
|
await closeForm();
|
||||||
|
});
|
||||||
|
|
||||||
await step('popup: всплывающая группа — behavior, открытие показывает содержимое', async () => {
|
await step('popup: всплывающая группа — behavior, открытие показывает содержимое', async () => {
|
||||||
const s = await navigateLink('Обработка.СтраницаНастроек');
|
const s = await navigateLink('Обработка.СтраницаНастроек');
|
||||||
const pg = (s.groups || []).find(g => g.name === 'ГруппаВсплывающая');
|
const pg = (s.groups || []).find(g => g.name === 'ГруппаВсплывающая');
|
||||||
|
|||||||
Reference in New Issue
Block a user