mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-29 08:01:02 +03:00
fix(web-test): состояние группы по контролу сворачивания, а не по вёрстке содержимого
На боевой форме вскрылись случаи, которые прежнее правило не покрывало. Замеры показали, что любая эвристика «по первому сиблингу за заголовком» нежизнеспособна: - служебная обёртка .logicGroupContainer пишется двумя способами (у таблицы <дочерний>#group_div, у вложенной группы <дочерний>_div) — знали только первый; - display самой обёртки плавает: block до первого тогла, none после; - первые узлы группы могут быть скрыты своей логикой, а видимое содержимое идёт дальше по цепочке — раскрытая группа читалась как свёрнутая, и постусловие роняло успешный клик. Теперь состояние берётся из контрола сворачивания: у варианта «картинка» — кадр gx спрайта hideshow у каретки (полярность обратна дереву в dom/grid.mjs), у варианта «гиперссылка» каретки нет, там принадлежность по отступу — дети группы смещены глубже её заголовка, свободный сосед стоит на уровне заголовка. База отсчёта — левый край блока заголовка (каретка сдвигает текст вправо), обход ограничен: у последней свёрнутой группы границы за ней нет, и первый видимый узел нашёлся через 107 сиблингов в чужой ветке формы. Клик по заголовку группы теперь сперва скроллит цель в вид: цель кликается по координатам, и ниже вьюпорта клик молча не доходил. Ответ на клик отдаёт clicked.group (техническое имя) и clicked.title (текущий заголовок): заголовок ключом быть не может — он повторяется между блоками формы и меняется при раскрытии (CollapsedRepresentationTitle), после чего клик по прежнему тексту не находит элемент. При смене заголовка hint говорит, чем кликать дальше. Фикстура: вложенная группа первым ребёнком, «скрыт первый узел» в двух вариантах контрола, группа с меняющимся заголовком в конце формы (уезжает за вьюпорт). Каждый кейс проверен на красноту без своей правки. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
006e64405c
commit
d0e81a1715
@@ -149,10 +149,17 @@ Returns current form structure. This is the primary way to understand what's on
|
|||||||
**navigation** — form navigation panel links (for objects with subordinate catalogs): `[{ name, active? }]`. Clickable via `clickElement()`. Only present when the form has a navigation panel (e.g. "Основное", "Объекты метаданных", "Подсистемы").
|
**navigation** — form navigation panel links (for objects with subordinate catalogs): `[{ name, active? }]`. Clickable via `clickElement()`. Only present when the form has a navigation panel (e.g. "Основное", "Объекты метаданных", "Подсистемы").
|
||||||
|
|
||||||
**groups** — collapsible and pop-up form groups: `[{ name, title, collapsed, behavior? }]`. `collapsed: true` means the group's content is hidden — part of the form is not shown until you expand it (common on settings pages like "Администрирование → Интернет-поддержка и сервисы"). `behavior: 'popup'` marks a pop-up group (content shows in a floating panel); absent for ordinary collapsible groups. Expand/collapse (or open/close a pop-up) by the group title with `clickElement`, same vocabulary as tree nodes: `{ expand: true }` reveals (idempotent), `{ expand: false }` hides, `{ toggle: true }` flips. After expanding, the group's content becomes readable in the next `getFormState()` (its fields/hyperlinks/texts appear). Plain (non-collapsible) groups are not listed.
|
**groups** — collapsible and pop-up form groups: `[{ name, title, collapsed, behavior? }]`. `collapsed: true` means the group's content is hidden — part of the form is not shown until you expand it (common on settings pages like "Администрирование → Интернет-поддержка и сервисы"). `behavior: 'popup'` marks a pop-up group (content shows in a floating panel); absent for ordinary collapsible groups. Expand/collapse (or open/close a pop-up) by the group title with `clickElement`, same vocabulary as tree nodes: `{ expand: true }` reveals (idempotent), `{ expand: false }` hides, `{ toggle: true }` flips. After expanding, the group's content becomes readable in the next `getFormState()` (its fields/hyperlinks/texts appear). Plain (non-collapsible) groups are not listed.
|
||||||
|
A group's title is not a stable key: the same caption repeats across blocks of a form, and a
|
||||||
|
group may swap it when expanded ("Показать детализацию" ↔ "Скрыть детализацию"), after which a
|
||||||
|
click by the old caption fails with "not found". The click result therefore reports `group` (the
|
||||||
|
group's technical name, which never changes and is accepted by `clickElement`) and `title` (its
|
||||||
|
caption right now) — use `group` when you plan to click the same group again.
|
||||||
```js
|
```js
|
||||||
const form = await getFormState();
|
const form = await getFormState();
|
||||||
// form.groups = [{ name: "ГруппаНовости", title: "Новости", collapsed: true }, ...]
|
// form.groups = [{ name: "ГруппаНовости", title: "Новости", collapsed: true }, ...]
|
||||||
await clickElement('Новости', { expand: true }); // reveal the group's content
|
const r = await clickElement('Новости', { expand: true }); // reveal the group's content
|
||||||
|
// r.clicked = { kind: 'formGroup', name: 'Новости', group: 'ГруппаНовости', title: 'Новости', toggled: true }
|
||||||
|
await clickElement(r.clicked.group, { expand: false }); // stable key — safe to reuse
|
||||||
```
|
```
|
||||||
|
|
||||||
**tables** — array of all visible grids: `[{ name, columns, rowCount, label? }]`. `label` is the visual group title shown on screen (e.g. "Входящие"), absent when grid has no visible title. Use `readTable()` for actual data.
|
**tables** — array of all visible grids: `[{ name, columns, rowCount, label? }]`. `label` is the visual group title shown on screen (e.g. "Входящие"), absent when grid has no visible title. Use `readTable()` for actual data.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export {
|
|||||||
closeCrossScript,
|
closeCrossScript,
|
||||||
readFormScript,
|
readFormScript,
|
||||||
findClickTargetScript,
|
findClickTargetScript,
|
||||||
|
scrollGroupIntoViewScript,
|
||||||
findFieldButtonScript,
|
findFieldButtonScript,
|
||||||
resolveFieldsScript,
|
resolveFieldsScript,
|
||||||
detectNewFormScript,
|
detectNewFormScript,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom shared v1.9 — embedded JS function constants
|
// web-test dom shared v1.10 — 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.
|
||||||
@@ -69,24 +69,39 @@ export const TEXT_CLICK_POINT_FN = `function textClickPoint(el) {
|
|||||||
* Collapsed state of a form group — single source of truth for getFormState().groups[]
|
* Collapsed state of a form group — single source of truth for getFormState().groups[]
|
||||||
* and the click-target resolver.
|
* and the click-target resolver.
|
||||||
*
|
*
|
||||||
* PopUp groups: the panel `<base>#panel_div` carries the state directly.
|
* 1C lays the form out FLAT: a group's content is not nested inside it but follows as
|
||||||
|
* absolutely-positioned siblings of `<base>#title_div`. Anything derived from "the first
|
||||||
|
* sibling" is unreliable — measured on live forms:
|
||||||
|
* • before a container child comes an empty `.logicGroupContainer` (height 0), spelled
|
||||||
|
* `<child>#group_div` for a table but `<child>_div` for a nested group;
|
||||||
|
* • that wrapper's own display FLIPS between runs (block before the first toggle, none
|
||||||
|
* after) — this is the "readings synced after the first toggle" from the bug report;
|
||||||
|
* • a group's leading nodes can stay `display:none` by their own logic (a table whose
|
||||||
|
* command bar is hidden) while the visible content sits further down the chain.
|
||||||
|
* Neither the wrappers' geometry nor the group's own `<base>_div` can serve as the signal:
|
||||||
|
* all of them are always zero-height.
|
||||||
*
|
*
|
||||||
* Collapsible groups: 1C lays the form out FLAT (content is not nested in the group, it
|
* Signals, in order:
|
||||||
* follows as siblings of `<base>#title_div`, absolutely positioned). Usually the first
|
* 1. PopUp — the panel `<base>#panel_div` carries the state directly.
|
||||||
* sibling is the group's own first child, and its `display` IS the state.
|
* 2. Caret (`ControlRepresentation=Picture`) — `<base>#titleBtn img` is the `hideshow`
|
||||||
*
|
* sprite, frame `gx`: 0 collapsed, non-zero expanded. Note the polarity is OPPOSITE
|
||||||
* But when that child is itself a container (a table, a nested group), 1C inserts a
|
* to tree nodes in dom/grid.mjs (gx=0 = expanded there) — different sprite.
|
||||||
* bookkeeping wrapper `<childName>#group_div.logicGroupContainer` FIRST — always
|
* 3. Otherwise (`TitleHyperlink`, which has no caret, no aria-expanded and no state class
|
||||||
* `display:block`, always `height:0`, and EMPTY: the real nodes
|
* on the title): ownership by INDENT. A group's children sit deeper than its title
|
||||||
* (`<childName>КоманднаяПанель_div`, `<childName>_div`, …) follow it as further siblings,
|
* (`#title_div` at left:12px → children at 22px), while a free element between groups
|
||||||
* they are not inside it. Reading that wrapper's display reported `collapsed:false` for a
|
* sits at the title's own level. So walk the siblings, skip hidden nodes and wrappers
|
||||||
* group that was genuinely collapsed, so `{expand:true}` became a silent no-op.
|
* (they are not positioned — left comes back `auto`), and the first VISIBLE node
|
||||||
*
|
* decides: deeper than the title ⇒ own content ⇒ expanded; same level or shallower
|
||||||
* So: on hitting the wrapper, walk the following siblings while they share its id prefix
|
* ⇒ that's already someone else, stop. Nothing own and visible ⇒ collapsed, which is
|
||||||
* (the prefix stops the walk at foreign nodes such as `separatePanelArea`, keeping the
|
* sound because a group with every element hidden is not rendered by the platform at all.
|
||||||
* "free element between groups" case safe) and report collapsed when none of them is
|
* The baseline is the leftmost part of the title BLOCK, not `#title_div` alone: with a
|
||||||
* visible. Neither the wrapper's own geometry nor the group's `<base>_div` can serve as
|
* caret the text is pushed right by its width (measured live: caret box 12px, title
|
||||||
* the signal — both are always zero-height.
|
* 33px, own children 22px), so anchoring on the title alone would read the group's own
|
||||||
|
* child as foreign. Only matters if a caret is present but signal 2 did not fire.
|
||||||
|
* The walk is capped: a group's own nodes sit right after its title, whereas the LAST
|
||||||
|
* collapsed group on a form has no boundary behind it at all — measured live, the first
|
||||||
|
* node with height came 107 siblings later, deep inside an unrelated branch, and would
|
||||||
|
* have been mistaken for the group's content.
|
||||||
*
|
*
|
||||||
* @param base element id prefix without suffix, e.g. `form1_ГруппаТовары`
|
* @param base element id prefix without suffix, e.g. `form1_ГруппаТовары`
|
||||||
* @returns `true` collapsed, `false` expanded, `null` when the layout is unrecognised.
|
* @returns `true` collapsed, `false` expanded, `null` when the layout is unrecognised.
|
||||||
@@ -94,21 +109,28 @@ export const TEXT_CLICK_POINT_FN = `function textClickPoint(el) {
|
|||||||
export const GROUP_STATE_FN = `function groupCollapsed(base) {
|
export const GROUP_STATE_FN = `function groupCollapsed(base) {
|
||||||
const panelDiv = document.getElementById(base + '#panel_div');
|
const panelDiv = document.getElementById(base + '#panel_div');
|
||||||
if (panelDiv) return getComputedStyle(panelDiv).display === 'none';
|
if (panelDiv) return getComputedStyle(panelDiv).display === 'none';
|
||||||
const titleDiv = document.getElementById(base + '#title_div');
|
const caret = document.querySelector('[id="' + base + '#titleBtn"] img');
|
||||||
const sib = titleDiv && titleDiv.nextElementSibling;
|
const src = caret ? (caret.getAttribute('src') || '') : '';
|
||||||
if (!sib) return null;
|
if (src.indexOf('hideshow') !== -1) {
|
||||||
const WRAP = '#group_div';
|
const gx = src.match(/[?&]gx=(\\d+)/);
|
||||||
if (sib.id && sib.id.endsWith(WRAP) && sib.classList.contains('logicGroupContainer')) {
|
if (gx) return gx[1] === '0';
|
||||||
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';
|
const titleDiv = document.getElementById(base + '#title_div');
|
||||||
|
if (!titleDiv) return null;
|
||||||
|
let titleLeft = parseFloat(getComputedStyle(titleDiv).left);
|
||||||
|
const caretDiv = document.getElementById(base + '#titleBtn_div');
|
||||||
|
const caretLeft = caretDiv ? parseFloat(getComputedStyle(caretDiv).left) : NaN;
|
||||||
|
if (!isNaN(caretLeft) && (isNaN(titleLeft) || caretLeft < titleLeft)) titleLeft = caretLeft;
|
||||||
|
if (isNaN(titleLeft)) return null;
|
||||||
|
let candidates = false, scanned = 0;
|
||||||
|
for (let n = titleDiv.nextElementSibling; n && scanned < 20; n = n.nextElementSibling, scanned++) {
|
||||||
|
if (n.offsetWidth === 0 && n.offsetHeight === 0) { candidates = true; continue; }
|
||||||
|
const left = parseFloat(getComputedStyle(n).left);
|
||||||
|
if (isNaN(left)) { candidates = true; continue; }
|
||||||
|
if (left > titleLeft) return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return candidates ? true : null;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -324,6 +324,39 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
|||||||
})()`;
|
})()`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll a collapsible group's title into view and return the FRESH click point for it.
|
||||||
|
*
|
||||||
|
* A group's title is clicked by coordinates (its click target is a nested label, not the
|
||||||
|
* element with the id), and `mouse.click` outside the viewport lands nowhere — the toggle
|
||||||
|
* silently did nothing. Seen live: the second «Показать детализацию» on a long form sat at
|
||||||
|
* y=848 with a viewport of 834. Playwright's own auto-scroll does not apply here because
|
||||||
|
* that path is selector-based, not coordinate-based.
|
||||||
|
*
|
||||||
|
* Same target choice as findClickTargetScript: the caret `#titleBtn` when visible (compact,
|
||||||
|
* aim at its centre), otherwise the title text via textClickPoint.
|
||||||
|
*
|
||||||
|
* @returns `{ x, y }` after scrolling, or `null` when the group is not on the form.
|
||||||
|
*/
|
||||||
|
export function scrollGroupIntoViewScript(formNum, groupName) {
|
||||||
|
const p = `form${formNum}_`;
|
||||||
|
return `(() => {
|
||||||
|
${TEXT_CLICK_POINT_FN}
|
||||||
|
const base = ${JSON.stringify(p)} + ${JSON.stringify(groupName)};
|
||||||
|
const tt = document.getElementById(base + '#title_text');
|
||||||
|
const btn = document.getElementById(base + '#titleBtn');
|
||||||
|
const btnVisible = btn && (btn.offsetWidth > 0 || btn.offsetHeight > 0);
|
||||||
|
const anchor = btnVisible ? btn : tt;
|
||||||
|
if (!anchor) return null;
|
||||||
|
anchor.scrollIntoView({ block: 'center' });
|
||||||
|
if (btnVisible) {
|
||||||
|
const r = btn.getBoundingClientRect();
|
||||||
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
|
}
|
||||||
|
return textClickPoint(tt);
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a field's action button (DLB, OB, CLR, CB) by fuzzy field name.
|
* Find a field's action button (DLB, OB, CLR, CB) by fuzzy field name.
|
||||||
* Returns { fieldName, buttonId, buttonType } or { error, available }.
|
* Returns { fieldName, buttonId, buttonType } or { error, available }.
|
||||||
|
|||||||
@@ -1,24 +1,38 @@
|
|||||||
// web-test forms/click-group v1.1 — click handler for collapsible/popup form-group titles.
|
// web-test forms/click-group v1.2 — 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:
|
||||||
// clickElement('<заголовок группы>', { expand: true }) — раскрыть (идемпотентно)
|
// clickElement('<group title>', { expand: true }) — reveal (idempotent)
|
||||||
// clickElement('<заголовок группы>', { expand: false }) — свернуть (идемпотентно)
|
// clickElement('<group title>', { expand: false }) — hide (idempotent)
|
||||||
// clickElement('<заголовок группы>', { toggle: true }) — переключить
|
// clickElement('<group title>', { toggle: true }) — flip
|
||||||
// clickElement('<заголовок группы>') — переключить (голый клик)
|
// clickElement('<group title>') — flip (bare click)
|
||||||
//
|
//
|
||||||
// target.collapsed приходит из findClickTargetScript (по display первого контент-сиблинга).
|
// target.collapsed comes from findClickTargetScript (groupCollapsed in dom/_shared.mjs).
|
||||||
|
|
||||||
|
import { page } from '../core/state.mjs';
|
||||||
|
import { scrollGroupIntoViewScript } from '../../dom.mjs';
|
||||||
import { waitForStable } from '../core/wait.mjs';
|
import { waitForStable } from '../core/wait.mjs';
|
||||||
import { modifierClick, returnFormState } from '../core/helpers.mjs';
|
import { modifierClick, returnFormState } from '../core/helpers.mjs';
|
||||||
import { shouldClickToggle } from '../table/grid-toggle.mjs';
|
import { shouldClickToggle } from '../table/grid-toggle.mjs';
|
||||||
|
|
||||||
|
// Group captions repeat across a form's blocks («Показать детализацию» in every block), and a
|
||||||
|
// collapsible group swaps its own caption when expanded (CollapsedRepresentationTitle:
|
||||||
|
// «Показать детализацию» ↔ «Скрыть детализацию»). Compare normalised so a real caption swap
|
||||||
|
// is not confused with nbsp/ё spelling differences.
|
||||||
|
const norm = (s) => (s || '').replace(/ /g, ' ').replace(/ё/gi, 'е').trim().toLowerCase();
|
||||||
|
|
||||||
export async function clickFormGroupTarget(target, ctx) {
|
export async function clickFormGroupTarget(target, ctx) {
|
||||||
const { formNum, modifier, toggle, expand } = ctx;
|
const { formNum, modifier, toggle, expand } = ctx;
|
||||||
// shouldClickToggle ждёт { isExpanded }; при неизвестном состоянии (undefined) кликаем всегда.
|
// shouldClickToggle expects { isExpanded }; with an unknown state (undefined) always click.
|
||||||
const state = target.collapsed == null ? null : { isExpanded: !target.collapsed };
|
const state = target.collapsed == null ? null : { isExpanded: !target.collapsed };
|
||||||
const shouldClick = shouldClickToggle(state, expand, toggle);
|
const shouldClick = shouldClickToggle(state, expand, toggle);
|
||||||
if (shouldClick) await modifierClick(target.x, target.y, modifier);
|
if (shouldClick) {
|
||||||
|
// The target is clicked by coordinates, and on a long form those go stale or fall outside
|
||||||
|
// the viewport — the click then silently does nothing. Scroll the title into view and take
|
||||||
|
// a fresh point.
|
||||||
|
const pt = await page.evaluate(scrollGroupIntoViewScript(formNum, target.label));
|
||||||
|
await modifierClick(pt?.x ?? target.x, pt?.y ?? target.y, modifier);
|
||||||
|
}
|
||||||
await waitForStable(formNum);
|
await waitForStable(formNum);
|
||||||
const result = await 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 } : {}) },
|
||||||
@@ -27,11 +41,22 @@ export async function clickFormGroupTarget(target, ctx) {
|
|||||||
: 'Group already in desired state.',
|
: 'Group already in desired state.',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Постусловие: клик, который ничего не переключил, — молчаливая ложь (то же правило, что
|
// The technical name is the stable key: it finds the entry in groups[] and clicks the same
|
||||||
// guard на disabled в core/click.mjs). Проверяем ТОЛЬКО когда клик реально был сделан и
|
// group again. The caption cannot do that — it changes when the group expands.
|
||||||
// состояние читаемо: при collapsed == null (нераспознанная вёрстка) сверять не с чем.
|
const after = (result.groups || []).find(g => g.name === target.label);
|
||||||
|
if (target.label) result.clicked.group = target.label;
|
||||||
|
if (after) {
|
||||||
|
result.clicked.title = after.title;
|
||||||
|
if (norm(after.title) !== norm(target.name)) {
|
||||||
|
result.hint += ` The group is now titled "${after.title}"`
|
||||||
|
+ ` — click it by that caption or by its technical name "${target.label}".`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postcondition: a click that toggled nothing is a silent lie (same rule as the disabled
|
||||||
|
// guard in core/click.mjs). Checked ONLY when a click actually happened and the state is
|
||||||
|
// readable: with collapsed == null (unrecognised layout) there is nothing to compare against.
|
||||||
if (shouldClick && target.collapsed != null) {
|
if (shouldClick && target.collapsed != null) {
|
||||||
const after = (result.groups || []).find(g => g.name === target.label);
|
|
||||||
if (after && after.collapsed === target.collapsed) {
|
if (after && after.collapsed === target.collapsed) {
|
||||||
throw new Error(`clickElement: group "${target.name}" did not toggle `
|
throw new Error(`clickElement: group "${target.name}" did not toggle `
|
||||||
+ `(collapsed stayed ${after.collapsed})`);
|
+ `(collapsed stayed ${after.collapsed})`);
|
||||||
|
|||||||
@@ -1372,6 +1372,47 @@ export const steps = [
|
|||||||
]},
|
]},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// ВЛОЖЕННАЯ ГРУППА ПЕРВЫМ РЕБЁНКОМ. Служебную обёртку .logicGroupContainer платформа
|
||||||
|
// пишет двумя способами: у таблицы — <дочерний>#group_div (группа выше), у вложенной
|
||||||
|
// группы — <дочерний>_div. Правило, узнающее только первую форму, на этой группе
|
||||||
|
// читает display самой обёртки (block) и рапортует «раскрыта» у свёрнутой.
|
||||||
|
{ group: 'collapsible', name: 'ГруппаСВложенной', title: 'Свёрнутая с вложенной группой',
|
||||||
|
collapsed: true, showTitle: true, children: [
|
||||||
|
{ group: 'vertical', name: 'ВложеннаяОбычная', title: 'Вложенная группа', showTitle: true,
|
||||||
|
children: [
|
||||||
|
{ label: 'ВнутриВложенной', title: 'Содержимое вложенной группы' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// ПЕРВЫЙ УЗЕЛ СКРЫТ СВОЕЙ ЛОГИКОЙ, видимое содержимое идёт дальше по цепочке сиблингов
|
||||||
|
// (на боевой форме так ведёт себя таблица со скрытой командной панелью). Правило
|
||||||
|
// «display первого контент-узла» здесь врёт в обе стороны: у раскрытой группы читает
|
||||||
|
// «свёрнута», из-за чего clickElement({expand}) считает клик несработавшим.
|
||||||
|
// Два варианта контрола: заголовок-гиперссылка и картинка-каретка.
|
||||||
|
{ group: 'collapsible', name: 'ГруппаСкрытыйПервый', title: 'Скрыт первый узел',
|
||||||
|
collapsed: true, showTitle: true, children: [
|
||||||
|
{ label: 'СкрытыйПервыйУзел', title: 'Эта декорация скрыта', visible: false },
|
||||||
|
{ label: 'ВидимыйВторойУзел', title: 'Видно только эту декорацию' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ group: 'collapsible', name: 'ГруппаСкрытыйПервыйКартинка', title: 'Скрыт первый узел (картинка)',
|
||||||
|
controlRepresentation: 'Picture', collapsed: true, showTitle: true, children: [
|
||||||
|
{ label: 'СкрытыйПервыйУзелК', title: 'Эта декорация скрыта (картинка)', visible: false },
|
||||||
|
{ label: 'ВидимыйВторойУзелК', title: 'Видно только эту декорацию (картинка)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// МЕНЯЮЩИЙСЯ ЗАГОЛОВОК (CollapsedRepresentationTitle): свёрнута — «Показать подробности»,
|
||||||
|
// раскрыта — «Скрыть подробности». Повторный клик по прежнему тексту уже не найдёт
|
||||||
|
// элемент, поэтому ответ на клик отдаёт техническое имя группы и её текущий заголовок.
|
||||||
|
// Стоит ПОСЛЕДНЕЙ на форме: с раскрытой группой-с-таблицей уезжает ниже вьюпорта, так что
|
||||||
|
// заодно покрывает клик по цели за пределами видимой области (координатный клик туда
|
||||||
|
// не доходил — молчаливый no-op).
|
||||||
|
{ group: 'collapsible', name: 'ГруппаМеняющийсяЗаголовок', title: 'Скрыть подробности',
|
||||||
|
collapsedTitle: 'Показать подробности', collapsed: true, showTitle: true, children: [
|
||||||
|
{ label: 'ВнутриПодробностей', title: 'Содержимое подробностей' },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' },
|
args: { '-JsonPath': '{inputFile}', '-OutputPath': '{workDir}/DataProcessors/СтраницаНастроек/Forms/ФормаОбработки/Ext/Form.xml' },
|
||||||
|
|||||||
@@ -101,6 +101,89 @@ export default async function({ navigateSection, openCommand, navigateLink, getF
|
|||||||
await closeForm();
|
await closeForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await step('вложенная группа первым ребёнком: обёртка вида <дочерний>_div', async () => {
|
||||||
|
// Служебную обёртку .logicGroupContainer платформа пишет двумя способами: у таблицы —
|
||||||
|
// <дочерний>#group_div, у вложенной группы — <дочерний>_div. Правило, знающее только
|
||||||
|
// первую форму, читает display обёртки (block) и рапортует «раскрыта» у свёрнутой.
|
||||||
|
const виден = (st) => (st.texts || []).some(t => /Содержимое вложенной группы/.test(t.value));
|
||||||
|
const s = await navigateLink('Обработка.СтраницаНастроек');
|
||||||
|
assert.equal(collapsedOf(s, 'ГруппаСВложенной'), true, 'свёрнута сразу после открытия');
|
||||||
|
assert.equal(виден(s), false, 'содержимое вложенной группы не видно');
|
||||||
|
|
||||||
|
let r = await clickElement('Свёрнутая с вложенной группой', { expand: true });
|
||||||
|
assert.equal(collapsedOf(r, 'ГруппаСВложенной'), false, 'раскрыта');
|
||||||
|
assert.equal(виден(r), true, 'содержимое вложенной группы появилось');
|
||||||
|
|
||||||
|
r = await clickElement('Свёрнутая с вложенной группой', { expand: true });
|
||||||
|
assert.equal(r.clicked.toggled, false, 'expand:true повторно — идемпотентно');
|
||||||
|
|
||||||
|
r = await clickElement('Свёрнутая с вложенной группой', { expand: false });
|
||||||
|
assert.equal(collapsedOf(r, 'ГруппаСВложенной'), true, 'свёрнута обратно');
|
||||||
|
assert.equal(виден(r), false, 'содержимое скрылось');
|
||||||
|
await closeForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('скрытый первый узел: состояние берётся не по первому сиблингу', async () => {
|
||||||
|
// Первый элемент группы скрыт своей логикой (visible:false), видимое содержимое — дальше
|
||||||
|
// по цепочке сиблингов. Так ведёт себя боевая форма с таблицей, у которой скрыта командная
|
||||||
|
// панель: чтение «display первого контент-узла» у РАСКРЫТОЙ группы даёт «свёрнута», и
|
||||||
|
// clickElement({expand}) считает свой клик несработавшим.
|
||||||
|
const пары = [
|
||||||
|
['Скрыт первый узел', 'ГруппаСкрытыйПервый', /^Видно только эту декорацию$/],
|
||||||
|
['Скрыт первый узел (картинка)', 'ГруппаСкрытыйПервыйКартинка', /Видно только эту декорацию \(картинка\)/],
|
||||||
|
];
|
||||||
|
for (const [заголовок, имя, маркер] of пары) {
|
||||||
|
const s = await navigateLink('Обработка.СтраницаНастроек');
|
||||||
|
const виден = (st) => (st.texts || []).some(t => маркер.test(t.value));
|
||||||
|
assert.equal(collapsedOf(s, имя), true, `${имя}: свёрнута после открытия`);
|
||||||
|
assert.equal(виден(s), false, `${имя}: содержимое скрыто`);
|
||||||
|
|
||||||
|
let r = await clickElement(заголовок, { expand: true });
|
||||||
|
assert.equal(collapsedOf(r, имя), false, `${имя}: раскрыта (первый узел так и остался скрытым)`);
|
||||||
|
assert.equal(виден(r), true, `${имя}: видимый узел появился`);
|
||||||
|
|
||||||
|
r = await clickElement(заголовок, { expand: true });
|
||||||
|
assert.equal(r.clicked.toggled, false, `${имя}: expand:true повторно — идемпотентно`);
|
||||||
|
|
||||||
|
r = await clickElement(заголовок, { expand: false });
|
||||||
|
assert.equal(collapsedOf(r, имя), true, `${имя}: свёрнута обратно`);
|
||||||
|
assert.equal(виден(r), false, `${имя}: содержимое скрылось`);
|
||||||
|
await closeForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('меняющийся заголовок + цель ниже вьюпорта: ответ даёт стабильный ключ', async () => {
|
||||||
|
// У группы задан CollapsedRepresentationTitle, поэтому текст заголовка меняется при
|
||||||
|
// раскрытии, и повторный клик по прежнему тексту падает с «not found». Ответ на клик
|
||||||
|
// отдаёт техническое имя (group) и текущий заголовок (title), а hint предупреждает о смене.
|
||||||
|
// Группа последняя на форме: раскрытая выше таблица уводит её за пределы окна — координатный
|
||||||
|
// клик туда раньше не доходил, теперь цель сперва скроллится в вид.
|
||||||
|
const G = 'ГруппаМеняющийсяЗаголовок';
|
||||||
|
const s = await navigateLink('Обработка.СтраницаНастроек');
|
||||||
|
assert.equal(collapsedOf(s, G), true, 'свёрнута после открытия');
|
||||||
|
assert.equal((s.groups || []).find(g => g.name === G)?.title, 'Показать подробности',
|
||||||
|
'в свёрнутом виде — заголовок свёрнутого представления');
|
||||||
|
await clickElement('Свёрнутая с таблицей', { expand: true }); // удлиняем форму
|
||||||
|
|
||||||
|
let r = await clickElement('Показать подробности', { expand: true });
|
||||||
|
assert.equal(r.clicked.toggled, true, 'кликнул (цель за вьюпортом — со скроллом)');
|
||||||
|
assert.equal(collapsedOf(r, G), false, 'раскрыта');
|
||||||
|
assert.equal(r.clicked.group, G, 'clicked.group — техническое имя группы');
|
||||||
|
assert.equal(r.clicked.title, 'Скрыть подробности', 'clicked.title — текущий заголовок');
|
||||||
|
assert.includes(r.hint, 'now titled "Скрыть подробности"', 'hint предупреждает о смене заголовка');
|
||||||
|
|
||||||
|
// Прежний текст больше не находится — ради этого в ответе и есть стабильный ключ.
|
||||||
|
await assert.throws(() => clickElement('Показать подробности', { expand: false }),
|
||||||
|
'клик по прежнему заголовку не проходит');
|
||||||
|
|
||||||
|
r = await clickElement(r.clicked.group, { expand: true });
|
||||||
|
assert.equal(r.clicked.toggled, false, 'по техническому имени expand идемпотентен');
|
||||||
|
r = await clickElement(G, { expand: false });
|
||||||
|
assert.equal(collapsedOf(r, G), true, 'свёрнута обратно по техническому имени');
|
||||||
|
assert.equal(r.clicked.title, 'Показать подробности', 'заголовок вернулся к свёрнутому');
|
||||||
|
await closeForm();
|
||||||
|
});
|
||||||
|
|
||||||
await step('растянутая гиперссылка: клик доходит до обработчика', async () => {
|
await step('растянутая гиперссылка: клик доходит до обработчика', async () => {
|
||||||
// Проверка соседнего с дефектом 2 случая. Ссылка растянута на всю ширину формы
|
// Проверка соседнего с дефектом 2 случая. Ссылка растянута на всю ширину формы
|
||||||
// (horizontalStretch + autoMaxWidth:false → контейнер 1295px), но промаха тут НЕ бывает:
|
// (horizontalStretch + autoMaxWidth:false → контейнер 1295px), но промаха тут НЕ бывает:
|
||||||
|
|||||||
Reference in New Issue
Block a user