mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-18 00:29:42 +03:00
feat(web-test): гард недоступных элементов + метка disabled в getFormState
clickElement/fillFields/selectValue бросали ложный успех при действии над недоступным (disabled) контролом — в 1С это no-op. Причина: резолвер цели клика не смотрел признак недоступности, который ридер getFormState уже знал. - резолвер клика снимает disabled (кнопки/frameButton/флажок/тумблер/поле), clickElement бросает `"X" is disabled` вместо тихого no-op; - fillFields и selectValue тоже бросают на недоступном поле/флажке/ссылке; - getFormState помечает disabled у frameButton, флажка, переключателя и тумблера (раньше был только у a.press-кнопок и полей ввода); - стенд: обработка ПроверкаДоступности с парами доступный/недоступный по всем типам контролов (в подсистеме Администрирование) + тест 23-availability; - SKILL.md: заметка про disabled у getFormState и throw у clickElement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -140,7 +140,7 @@ Returns current form structure. This is the primary way to understand what's on
|
||||
|
||||
**openTabs** — array of `{ name, active? }` from the open-windows tab bar. Only present when the tab bar is enabled in 1C settings. Do NOT rely on this — use `formCount`/`openForms` instead.
|
||||
|
||||
**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields)
|
||||
**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields), `disabled?` (control is unavailable). `buttons[]` carry `disabled?` too.
|
||||
|
||||
**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. "Основное", "Объекты метаданных", "Подсистемы").
|
||||
|
||||
@@ -240,6 +240,8 @@ Sections + all open tabs.
|
||||
#### `clickElement(text, { dblclick?, table?, expand?, modifier?, scroll? })` → form state
|
||||
Click button, hyperlink, tab, navigation panel link, or grid row (fuzzy match).
|
||||
|
||||
**Disabled controls throw.** `clickElement`, `fillFields`, and `selectValue` throw `"X" is disabled` on an unavailable control instead of reporting a fake success — check `getFormState().buttons[].disabled` / `fields[].disabled` first.
|
||||
|
||||
- `table` — scope button search to a specific grid's command panel (by name from `tables[]`):
|
||||
```js
|
||||
await clickElement('Добавить', { table: 'Исходящие' }); // clicks "Добавить" near "Исходящие" grid
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test dom shared v1.2 — embedded JS function constants
|
||||
// web-test dom shared v1.3 — embedded JS function constants
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Shared function strings embedded into page.evaluate() generators.
|
||||
@@ -214,6 +214,7 @@ function readForm(p) {
|
||||
type: 'checkbox'
|
||||
};
|
||||
if (label && label !== name) field.label = label;
|
||||
if (el.classList.contains('checkboxDisabled')) field.disabled = true;
|
||||
fields.push(field);
|
||||
});
|
||||
|
||||
@@ -249,6 +250,7 @@ function readForm(p) {
|
||||
options: options.map(o => o.label)
|
||||
};
|
||||
if (label && label !== name) field.label = label;
|
||||
if (document.getElementById(p + name)?.classList.contains('radioDisabled')) field.disabled = true;
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
@@ -277,15 +279,21 @@ function readForm(p) {
|
||||
const text = nbsp(el.innerText?.trim() || '');
|
||||
const idName = el.id?.replace(p, '') || '';
|
||||
if (!text && !idName) return;
|
||||
buttons.push({ name: text || idName, frame: true });
|
||||
// frameButton disabled uses the same class as a.press buttons (pressDisabled).
|
||||
const btn = { name: text || idName, frame: true };
|
||||
if (el.classList.contains('pressDisabled')) btn.disabled = true;
|
||||
buttons.push(btn);
|
||||
});
|
||||
|
||||
// Tumbler items
|
||||
// Tumbler items. Disabled state lives on the group element .frameTumbler
|
||||
// (class tumblerDisabled), not on the individual segments.
|
||||
document.querySelectorAll('[id^="' + p + '"].tumblerItem').forEach(el => {
|
||||
if (el.offsetWidth === 0) return;
|
||||
const text = el.innerText?.trim();
|
||||
const idName = el.id?.replace(p, '') || '';
|
||||
buttons.push({ name: text || idName, tumbler: true });
|
||||
const btn = { name: text || idName, tumbler: true };
|
||||
if (el.closest('.frameTumbler')?.classList.contains('tumblerDisabled')) btn.disabled = true;
|
||||
buttons.push(btn);
|
||||
});
|
||||
|
||||
// Tabs — scoped to form by checking ancestor IDs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test dom/forms v1.9 — form detection, content read, click-target/field-button resolution
|
||||
// web-test dom/forms v1.10 — form detection, content read, click-target/field-button resolution
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { DETECT_FORM_FN, READ_FORM_FN, ROW_CLICK_POINT_FN } from './_shared.mjs';
|
||||
|
||||
@@ -89,6 +89,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
if (!text && !el.classList.contains('pressCommand')) return;
|
||||
const isSubmenu = /^(?:Подменю|allActions)/i.test(idName);
|
||||
const item = { id: el.id, name: text || idName, label: idName, kind: isSubmenu ? 'submenu' : 'button' };
|
||||
if (el.classList.contains('pressDisabled')) item.disabled = true;
|
||||
// Icon-only buttons: use tooltip for fuzzy match (1C puts title on parent .framePress)
|
||||
if (!text) { const tip = norm(el.title || el.parentElement?.title || ''); if (tip) item.tooltip = tip; }
|
||||
items.push(item);
|
||||
@@ -106,14 +107,19 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
const text = norm(el.innerText);
|
||||
const idName = el.id.replace(p, '');
|
||||
if (!text && !idName) return;
|
||||
items.push({ id: el.id, name: text || idName, label: text ? '' : idName, kind: 'frameButton' });
|
||||
const item = { id: el.id, name: text || idName, label: text ? '' : idName, kind: 'frameButton' };
|
||||
if (el.classList.contains('pressDisabled')) item.disabled = true;
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
// Tumbler items (toggle switch segments)
|
||||
// Tumbler items (toggle switch segments). Disabled state lives on the group
|
||||
// element .frameTumbler (class tumblerDisabled), not on the segments themselves.
|
||||
[...document.querySelectorAll('[id^="' + p + '"].tumblerItem')].filter(el => el.offsetWidth > 0).forEach(el => {
|
||||
const idName = el.id.replace(p, '');
|
||||
const text = norm(el.innerText);
|
||||
items.push({ id: el.id, name: text || idName, label: idName, kind: 'tumbler' });
|
||||
const item = { id: el.id, name: text || idName, label: idName, kind: 'tumbler' };
|
||||
if (el.closest('.frameTumbler')?.classList.contains('tumblerDisabled')) item.disabled = true;
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
// Checkboxes (div.checkbox) — match by label or internal name
|
||||
@@ -121,7 +127,9 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
const idName = el.id.replace(p, '');
|
||||
const titleEl = document.getElementById(p + idName + '#title_text');
|
||||
const label = norm(titleEl?.innerText || '').replace(/:/g, '').trim();
|
||||
items.push({ id: el.id, name: label || idName, label: idName, kind: 'checkbox' });
|
||||
const item = { id: el.id, name: label || idName, label: idName, kind: 'checkbox' };
|
||||
if (el.classList.contains('checkboxDisabled')) item.disabled = true;
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
// Tabs (scoped to form)
|
||||
@@ -174,7 +182,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
if (!cf) cf = containerItems.find(i => i.label && i.label.toLowerCase() === target);
|
||||
if (!cf && target.length >= 4) cf = containerItems.find(i => i.name.toLowerCase().includes(target));
|
||||
if (!cf && target.length >= 4) cf = containerItems.find(i => i.label && i.label.toLowerCase().includes(target));
|
||||
if (cf) { const res = { id: cf.id, kind: cf.kind, name: cf.name }; if (cf.x != null) { res.x = cf.x; res.y = cf.y; } return res; }
|
||||
if (cf) { const res = { id: cf.id, kind: cf.kind, name: cf.name }; if (cf.disabled) res.disabled = true; if (cf.x != null) { res.x = cf.x; res.y = cf.y; } return res; }
|
||||
// Fallback: filter by gridName id-prefix (e.g. ИсходящиеКоманднаяПанель_Добавить)
|
||||
const gridName = gridEl.id ? gridEl.id.replace(p, '') : '';
|
||||
if (gridName) {
|
||||
@@ -182,7 +190,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
let pf = prefixItems.find(i => i.name.toLowerCase() === target);
|
||||
if (!pf && target.length >= 4) pf = prefixItems.find(i => i.label && i.label.toLowerCase().includes(target));
|
||||
if (!pf && target.length >= 4) pf = prefixItems.find(i => i.name.toLowerCase().includes(target));
|
||||
if (pf) { const res = { id: pf.id, kind: pf.kind, name: pf.name }; if (pf.x != null) { res.x = pf.x; res.y = pf.y; } return res; }
|
||||
if (pf) { const res = { id: pf.id, kind: pf.kind, name: pf.name }; if (pf.disabled) res.disabled = true; if (pf.x != null) { res.x = pf.x; res.y = pf.y; } return res; }
|
||||
}
|
||||
}
|
||||
// Fall through to unscoped search
|
||||
@@ -202,6 +210,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
|
||||
if (found) {
|
||||
const res = { id: found.id, kind: found.kind, name: found.name };
|
||||
if (found.disabled) res.disabled = true;
|
||||
if (found.x != null) { res.x = found.x; res.y = found.y; }
|
||||
return res;
|
||||
}
|
||||
@@ -262,7 +271,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
const idName = el.id.replace(p, '').replace(/_i\\d+$/, '');
|
||||
const titleEl = document.getElementById(p + idName + '#title_text') || document.getElementById(p + idName + '#title_div');
|
||||
const label = norm(titleEl?.innerText || '').replace(/:/g, '').trim();
|
||||
fields.push({ id: el.id, name: idName, label });
|
||||
fields.push({ id: el.id, name: idName, label, disabled: !!el.disabled });
|
||||
});
|
||||
let ff = fields.find(f => f.label && f.label.toLowerCase() === target);
|
||||
if (!ff) ff = fields.find(f => f.name.toLowerCase() === target);
|
||||
@@ -270,7 +279,7 @@ export function findClickTargetScript(formNum, text, { tableName, gridSelector }
|
||||
if (!ff) ff = fields.find(f => f.name.toLowerCase().startsWith(target));
|
||||
if (!ff && target.length >= 4) ff = fields.find(f => f.label && f.label.toLowerCase().includes(target));
|
||||
if (!ff && target.length >= 4) ff = fields.find(f => f.name.toLowerCase().includes(target));
|
||||
if (ff) return { id: ff.id, kind: 'field', name: ff.label || ff.name };
|
||||
if (ff) return { id: ff.id, kind: 'field', name: ff.label || ff.name, ...(ff.disabled ? { disabled: true } : {}) };
|
||||
}
|
||||
|
||||
const available = items.map(i => i.tooltip ? i.name + ' [' + i.tooltip + ']' : i.name).filter(Boolean);
|
||||
@@ -344,6 +353,11 @@ export function findFieldButtonScript(formNum, fieldName, buttonSuffix = 'DLB')
|
||||
}
|
||||
const result = { fieldName: found.name, buttonId: btnId, buttonType: suffix };
|
||||
if (dcsCheckbox) result.dcsCheckbox = { inputId: dcsCheckbox.inputId };
|
||||
// Disabled reference field keeps its DLB/CB visible (won't hit button_not_found),
|
||||
// so flag disabled off the field input for selectValue to guard against a silent no-op.
|
||||
const fieldInput = document.getElementById(p + found.name + '_i0')
|
||||
|| document.querySelector('input[id^="' + p + found.name + '_i"]');
|
||||
if (fieldInput?.disabled) result.disabled = true;
|
||||
return result;
|
||||
})()`;
|
||||
}
|
||||
@@ -370,6 +384,7 @@ export function resolveFieldsScript(formNum, fields) {
|
||||
|| document.getElementById(p + name + '#title_div');
|
||||
const label = (titleEl?.innerText?.trim() || '').replace(/\\n/g, ' ').replace(/:$/, '');
|
||||
const last = { inputId: el.id, name, label };
|
||||
if (el.disabled) last.disabled = true;
|
||||
if (document.getElementById(p + name + '_DLB')?.offsetWidth > 0) last.hasSelect = true;
|
||||
const cbEl = document.getElementById(p + name + '_CB');
|
||||
if (cbEl?.offsetWidth > 0) {
|
||||
@@ -386,7 +401,9 @@ export function resolveFieldsScript(formNum, fields) {
|
||||
const titleEl = document.getElementById(p + name + '#title_text');
|
||||
const label = (titleEl?.innerText?.trim() || '').replace(/\\n/g, ' ').replace(/:$/, '');
|
||||
const checked = el.classList.contains('checked') || el.classList.contains('checkboxOn') || el.classList.contains('select');
|
||||
allFields.push({ inputId: el.id, name, label, isCheckbox: true, checked });
|
||||
const cb = { inputId: el.id, name, label, isCheckbox: true, checked };
|
||||
if (el.classList.contains('checkboxDisabled')) cb.disabled = true;
|
||||
allFields.push(cb);
|
||||
});
|
||||
// Radio button groups — base element = option 0, others are #N#radio
|
||||
const radioSeen = new Set();
|
||||
@@ -415,7 +432,10 @@ export function resolveFieldsScript(formNum, fields) {
|
||||
const textEl = document.getElementById(p + groupName + '#' + i + '#radio_text');
|
||||
options.push({ index: i, label: textEl?.innerText?.trim() || '', selected: opt.classList.contains('select') });
|
||||
}
|
||||
allFields.push({ inputId: p + groupName, name: groupName, label, isRadio: true, options });
|
||||
const groupEl = document.getElementById(p + groupName);
|
||||
const radio = { inputId: p + groupName, name: groupName, label, isRadio: true, options };
|
||||
if (groupEl?.classList.contains('radioDisabled')) radio.disabled = true;
|
||||
allFields.push(radio);
|
||||
});
|
||||
|
||||
// Build DCS pairs: checkbox label → paired value field
|
||||
@@ -451,6 +471,7 @@ export function resolveFieldsScript(formNum, fields) {
|
||||
|
||||
if (found) {
|
||||
const entry = { field: fieldName, inputId: found.inputId, name: found.name, label: found.label };
|
||||
if (found.disabled) entry.disabled = true;
|
||||
if (found.isCheckbox) { entry.isCheckbox = true; entry.checked = found.checked; }
|
||||
if (found.isRadio) { entry.isRadio = true; entry.options = found.options; }
|
||||
if (found.hasSelect) entry.hasSelect = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test core/click v1.22 — clickElement dispatcher: routes to spreadsheet / popup / grid-row / form-element / field-focus handlers by target kind.
|
||||
// web-test core/click v1.23 — clickElement dispatcher: routes to spreadsheet / popup / grid-row / form-element / field-focus handlers by target kind.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import { page, ensureConnected, highlightMode } from './state.mjs';
|
||||
@@ -116,7 +116,12 @@ export async function clickElement(text, { dblclick, table, toggle, expand, modi
|
||||
throw new Error(`clickElement: "${text}" not found. Available: ${target.available?.join(', ') || 'none'}`);
|
||||
}
|
||||
|
||||
// 4. Dispatch to the right handler by target kind.
|
||||
// 4. Guard: a disabled target (button/frameButton/checkbox/tumbler/field) is a silent
|
||||
// no-op in 1C — clicking it does nothing. Fail loud instead of masquerading success.
|
||||
// Availability is checkable up front via getFormState().buttons[].disabled / fields[].disabled.
|
||||
if (target.disabled) throw new Error(`clickElement: "${text}" is disabled`);
|
||||
|
||||
// 5. Dispatch to the right handler by target kind.
|
||||
const ctx = { formNum, modifier, dblclick, toggle, expand, timeout, table, gridSelector };
|
||||
if (target.kind === 'gridGroup' || target.kind === 'gridParent') return await clickGridGroupTarget(target, ctx);
|
||||
if (target.kind === 'gridTreeNode') return await clickGridTreeNodeTarget(target, ctx);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test forms/fill v1.20 — Fill form fields by name (text/checkbox/date/number/dropdown/reference; array → multi-select via selectValue). Delegates references to selectValue / fillReferenceField.
|
||||
// web-test forms/fill v1.21 — Fill form fields by name (text/checkbox/date/number/dropdown/reference; array → multi-select via selectValue). Delegates references to selectValue / fillReferenceField.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -33,6 +33,12 @@ export async function fillFields(fields) {
|
||||
results.push(r);
|
||||
continue;
|
||||
}
|
||||
// Disabled field: filling/toggling it is a silent no-op in 1C. Fail loud via the
|
||||
// standard failure aggregation below instead of reporting a fake success.
|
||||
if (r.disabled) {
|
||||
results.push({ field: r.field, error: 'disabled', message: `field "${r.field}" is disabled` });
|
||||
continue;
|
||||
}
|
||||
// Array value → multi-select. Delegate to selectValue's array branch (auto-detects
|
||||
// the surface) so fillFields({field:[...]}) works the same as selectValue(field,[...]).
|
||||
if (Array.isArray(fields[r.field])) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test forms/select-value v1.33 — Reference & composite-type value selection: selectValue (+ array multi-select), fillReferenceField, selection/type-dialog pickers.
|
||||
// web-test forms/select-value v1.34 — Reference & composite-type value selection: selectValue (+ array multi-select), fillReferenceField, selection/type-dialog pickers.
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import {
|
||||
@@ -942,6 +942,9 @@ export async function selectValue(fieldName, searchText, { type } = {}) {
|
||||
btn = await page.evaluate(findFieldButtonScript(formNum, fieldName, 'CB'));
|
||||
}
|
||||
if (btn?.error) return btn;
|
||||
// Disabled reference field keeps its pick button visible, so the click would silently
|
||||
// do nothing. Fail loud, consistent with clickElement's disabled guard.
|
||||
if (btn?.disabled) throw new Error(`selectValue: field "${btn.fieldName || fieldName}" is disabled`);
|
||||
if (highlightMode) try { await highlight(fieldName); await page.waitForTimeout(500); await unhighlight(); } catch {}
|
||||
try {
|
||||
|
||||
|
||||
@@ -500,6 +500,35 @@ export const steps = [
|
||||
validate: { script: 'meta-validate/scripts/meta-validate', flag: '-ObjectPath', path: 'DataProcessors/БезшапочнаяТаблица' },
|
||||
},
|
||||
|
||||
// Обработка ПроверкаДоступности — форма с парами «доступный / недоступный»
|
||||
// элементов каждого типа (кнопка командной панели, обычная кнопка, поле ввода,
|
||||
// флажок, переключатель). Недоступность задаётся декларативно (disabled:true →
|
||||
// <Enabled>false</Enabled>). Для регресса: clickElement по недоступной кнопке
|
||||
// должен бросать ошибку, а getFormState — корректно помечать disabled у всех
|
||||
// типов. Реквизиты парные (А=доступный, Б=недоступный), чтобы у элементов не
|
||||
// совпадали companion-имена.
|
||||
{
|
||||
name: 'meta-compile: Обработка ПроверкаДоступности',
|
||||
script: 'meta-compile/scripts/meta-compile',
|
||||
input: {
|
||||
type: 'DataProcessor', name: 'ПроверкаДоступности',
|
||||
attributes: [
|
||||
{ name: 'СтрокаДоступная', type: 'String', length: 50 },
|
||||
{ name: 'СтрокаНедоступная', type: 'String', length: 50 },
|
||||
{ name: 'ФлажокДоступный', type: 'Boolean' },
|
||||
{ name: 'ФлажокНедоступный', type: 'Boolean' },
|
||||
{ name: 'ВидДоступный', type: 'EnumRef.ВидыНоменклатуры' },
|
||||
{ name: 'ВидНедоступный', type: 'EnumRef.ВидыНоменклатуры' },
|
||||
{ name: 'ТумблерДоступный', type: 'EnumRef.СпособыУчёта' },
|
||||
{ name: 'ТумблерНедоступный', type: 'EnumRef.СпособыУчёта' },
|
||||
{ name: 'КонтрагентДоступный', type: 'CatalogRef.Контрагенты' },
|
||||
{ name: 'КонтрагентНедоступный', type: 'CatalogRef.Контрагенты' },
|
||||
],
|
||||
},
|
||||
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
|
||||
validate: { script: 'meta-validate/scripts/meta-validate', flag: '-ObjectPath', path: 'DataProcessors/ПроверкаДоступности' },
|
||||
},
|
||||
|
||||
// Отчёт ОстаткиТоваров
|
||||
{
|
||||
name: 'meta-compile: Отчёт ОстаткиТоваров',
|
||||
@@ -884,6 +913,111 @@ export const steps = [
|
||||
`,
|
||||
},
|
||||
|
||||
// Форма обработки ПроверкаДоступности — доступные/недоступные элементы каждого типа.
|
||||
// Командная панель формы (autoCmdBar) и обычные кнопки в группе; поля/флажки/
|
||||
// переключатели парами. Недоступные помечены disabled:true (→ <Enabled>false</Enabled>),
|
||||
// 1С рисует их серыми: кнопки получают класс pressDisabled.
|
||||
{
|
||||
name: 'form-add: Форма обработки ПроверкаДоступности',
|
||||
script: 'form-add/scripts/form-add',
|
||||
args: { '-ObjectPath': '{workDir}/DataProcessors/ПроверкаДоступности.xml', '-FormName': 'ФормаОбработки' },
|
||||
},
|
||||
{
|
||||
name: 'form-compile: Форма обработки ПроверкаДоступности',
|
||||
script: 'form-compile/scripts/form-compile',
|
||||
input: {
|
||||
title: 'Проверка доступности',
|
||||
attributes: [
|
||||
{ name: 'Объект', type: 'DataProcessorObject.ПроверкаДоступности', main: true },
|
||||
],
|
||||
elements: [
|
||||
// Командная панель формы: доступная + недоступная кнопка.
|
||||
{ autoCmdBar: 'ФормаКоманднаяПанель', autofill: false, children: [
|
||||
{ button: 'КомандаПанелиДоступна', command: 'КомандаПанелиДоступна', title: 'Доступная команда' },
|
||||
{ button: 'КомандаПанелиНедоступна', command: 'КомандаПанелиНедоступна', title: 'Недоступная команда', disabled: true },
|
||||
]},
|
||||
// Обычные кнопки (в группе на теле формы, вне командной панели).
|
||||
{ group: 'horizontal', name: 'ГруппаОбычныхКнопок', title: 'Обычные кнопки', children: [
|
||||
{ button: 'КнопкаДоступна', command: 'КомандаКнопкаДоступна', title: 'Доступная кнопка' },
|
||||
{ button: 'КнопкаНедоступна', command: 'КомандаКнопкаНедоступна', title: 'Недоступная кнопка', disabled: true },
|
||||
]},
|
||||
// Поля ввода.
|
||||
{ input: 'СтрокаДоступная', path: 'Объект.СтрокаДоступная', title: 'Строка доступная' },
|
||||
{ input: 'СтрокаНедоступная', path: 'Объект.СтрокаНедоступная', title: 'Строка недоступная', disabled: true },
|
||||
// Флажки.
|
||||
{ check: 'ФлажокДоступный', path: 'Объект.ФлажокДоступный', title: 'Флажок доступный' },
|
||||
{ check: 'ФлажокНедоступный', path: 'Объект.ФлажокНедоступный', title: 'Флажок недоступный', disabled: true },
|
||||
// Переключатели.
|
||||
{ radio: 'ВидДоступный', path: 'Объект.ВидДоступный',
|
||||
title: 'Вид доступный', radioButtonType: 'RadioButtons', titleLocation: 'Top',
|
||||
choiceList: [
|
||||
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Товар', presentation: 'Товар' },
|
||||
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Услуга', presentation: 'Услуга' },
|
||||
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Работа', presentation: 'Работа' },
|
||||
],
|
||||
},
|
||||
{ radio: 'ВидНедоступный', path: 'Объект.ВидНедоступный',
|
||||
title: 'Вид недоступный', radioButtonType: 'RadioButtons', titleLocation: 'Top', disabled: true,
|
||||
choiceList: [
|
||||
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Товар', presentation: 'Товар' },
|
||||
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Услуга', presentation: 'Услуга' },
|
||||
{ value: 'Enum.ВидыНоменклатуры.EnumValue.Работа', presentation: 'Работа' },
|
||||
],
|
||||
},
|
||||
// Тумблеры (переключатель с видом Tumbler — рендерится иначе, чем RadioButtons).
|
||||
{ radio: 'ТумблерДоступный', path: 'Объект.ТумблерДоступный',
|
||||
title: 'Тумблер доступный', radioButtonType: 'Tumbler', titleLocation: 'Top',
|
||||
choiceList: [
|
||||
{ value: 'Enum.СпособыУчёта.EnumValue.ПоСреднему', presentation: 'По среднему' },
|
||||
{ value: 'Enum.СпособыУчёта.EnumValue.ФИФО', presentation: 'ФИФО' },
|
||||
],
|
||||
},
|
||||
{ radio: 'ТумблерНедоступный', path: 'Объект.ТумблерНедоступный',
|
||||
title: 'Тумблер недоступный', radioButtonType: 'Tumbler', titleLocation: 'Top', disabled: true,
|
||||
choiceList: [
|
||||
{ value: 'Enum.СпособыУчёта.EnumValue.ПоСреднему', presentation: 'По среднему' },
|
||||
{ value: 'Enum.СпособыУчёта.EnumValue.ФИФО', presentation: 'ФИФО' },
|
||||
],
|
||||
},
|
||||
// Ссылочные поля (для selectValue — выбор через DLB/CB-кнопки).
|
||||
{ input: 'КонтрагентДоступный', path: 'Объект.КонтрагентДоступный', title: 'Контрагент доступный' },
|
||||
{ input: 'КонтрагентНедоступный', path: 'Объект.КонтрагентНедоступный', title: 'Контрагент недоступный', disabled: true },
|
||||
],
|
||||
commands: [
|
||||
{ name: 'КомандаПанелиДоступна', action: 'КомандаПанелиДоступна' },
|
||||
{ name: 'КомандаПанелиНедоступна', action: 'КомандаПанелиНедоступна' },
|
||||
{ name: 'КомандаКнопкаДоступна', action: 'КомандаКнопкаДоступна' },
|
||||
{ name: 'КомандаКнопкаНедоступна', action: 'КомандаКнопкаНедоступна' },
|
||||
],
|
||||
},
|
||||
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' },
|
||||
},
|
||||
{
|
||||
name: 'writeFile: ПроверкаДоступности form Module.bsl',
|
||||
writeFile: 'DataProcessors/ПроверкаДоступности/Forms/ФормаОбработки/Ext/Form/Module.bsl',
|
||||
content: `&НаКлиенте
|
||||
Процедура КомандаПанелиДоступна(Команда)
|
||||
\tСообщить("Команда панели (доступна)");
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура КомандаПанелиНедоступна(Команда)
|
||||
\tСообщить("Команда панели (недоступна)");
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура КомандаКнопкаДоступна(Команда)
|
||||
\tСообщить("Обычная кнопка (доступна)");
|
||||
КонецПроцедуры
|
||||
|
||||
&НаКлиенте
|
||||
Процедура КомандаКнопкаНедоступна(Команда)
|
||||
\tСообщить("Обычная кнопка (недоступна)");
|
||||
КонецПроцедуры
|
||||
`,
|
||||
},
|
||||
|
||||
// Форма обработки ДеревоНоменклатуры — tree-grid с двумя колонками
|
||||
{
|
||||
name: 'form-add: Форма обработки ДеревоНоменклатуры',
|
||||
@@ -1479,6 +1613,7 @@ export const steps = [
|
||||
'Constant.ОсновнаяВалюта',
|
||||
'DataProcessor.ТестовыеОшибки',
|
||||
'DataProcessor.ДеревоНоменклатуры',
|
||||
'DataProcessor.ПроверкаДоступности',
|
||||
],
|
||||
},
|
||||
args: { '-DefinitionFile': '{inputFile}', '-OutputDir': '{workDir}' },
|
||||
@@ -1502,6 +1637,7 @@ export const steps = [
|
||||
'DataProcessor.ДеревоНоменклатуры: Use View',
|
||||
'DataProcessor.МножественныйВыбор: Use View',
|
||||
'DataProcessor.БезшапочнаяТаблица: Use View',
|
||||
'DataProcessor.ПроверкаДоступности: Use View',
|
||||
],
|
||||
},
|
||||
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
export const name = 'Доступность элементов: getFormState помечает disabled, clickElement/fillFields/selectValue бросают';
|
||||
export const tags = ['availability', 'disabled', 'click', 'fill', 'smoke'];
|
||||
export const timeout = 120000;
|
||||
|
||||
const findBtn = (s, name) => s.buttons?.find(b => b.name === name);
|
||||
const findField = (s, name) => s.fields?.find(f => f.name === name || f.label === name);
|
||||
|
||||
export default async function({ navigateLink, clickElement, fillFields, selectValue, getFormState, closeForm, assert, step, log }) {
|
||||
|
||||
// Один раз открываем форму обработки; disabled-гарды бросают ДО действия и состояние
|
||||
// не портят, поэтому переиспользуем её между шагами, закрываем в конце.
|
||||
await step('getFormState помечает disabled по всем типам контролов', async () => {
|
||||
const s = await navigateLink('Обработка.ПроверкаДоступности');
|
||||
log('buttons: ' + JSON.stringify(s.buttons));
|
||||
|
||||
// Кнопка командной панели (a.press)
|
||||
assert.ok(!findBtn(s, 'Доступная команда')?.disabled, 'доступная команда — без disabled');
|
||||
assert.equal(findBtn(s, 'Недоступная команда')?.disabled, true, 'недоступная команда — disabled');
|
||||
|
||||
// Обычная кнопка (frameButton)
|
||||
assert.ok(!findBtn(s, 'Доступная кнопка')?.disabled, 'доступная кнопка — без disabled');
|
||||
assert.equal(findBtn(s, 'Недоступная кнопка')?.disabled, true, 'недоступная кнопка (frameButton) — disabled');
|
||||
|
||||
// Поле ввода (нативный disabled)
|
||||
assert.ok(!findField(s, 'СтрокаДоступная')?.disabled, 'доступное поле — без disabled');
|
||||
assert.equal(findField(s, 'СтрокаНедоступная')?.disabled, true, 'недоступное поле — disabled');
|
||||
|
||||
// Флажок (checkboxDisabled)
|
||||
assert.ok(!findField(s, 'ФлажокДоступный')?.disabled, 'доступный флажок — без disabled');
|
||||
assert.equal(findField(s, 'ФлажокНедоступный')?.disabled, true, 'недоступный флажок — disabled');
|
||||
|
||||
// Переключатель RadioButtons (radioDisabled)
|
||||
assert.ok(!findField(s, 'ВидДоступный')?.disabled, 'доступный переключатель — без disabled');
|
||||
assert.equal(findField(s, 'ВидНедоступный')?.disabled, true, 'недоступный переключатель — disabled');
|
||||
|
||||
// Ссылочное поле (нативный disabled на input)
|
||||
assert.ok(!findField(s, 'КонтрагентДоступный')?.disabled, 'доступное ссыл. поле — без disabled');
|
||||
assert.equal(findField(s, 'КонтрагентНедоступный')?.disabled, true, 'недоступное ссыл. поле — disabled');
|
||||
|
||||
// Тумблер (tumblerDisabled на группе .frameTumbler) — сегменты идут в buttons[] с tumbler:true;
|
||||
// у недоступного тумблера оба сегмента помечены disabled.
|
||||
const disabledTumblers = (s.buttons || []).filter(b => b.tumbler && b.disabled);
|
||||
assert.equal(disabledTumblers.length, 2, 'оба сегмента недоступного тумблера помечены disabled');
|
||||
const enabledTumblers = (s.buttons || []).filter(b => b.tumbler && !b.disabled);
|
||||
assert.equal(enabledTumblers.length, 2, 'сегменты доступного тумблера — без disabled');
|
||||
});
|
||||
|
||||
await step('clickElement бросает на недоступной кнопке/frameButton/флажке, проходит на доступной', async () => {
|
||||
let err = null;
|
||||
try { await clickElement('Недоступная команда'); } catch (e) { err = e; }
|
||||
assert.ok(err, 'клик по недоступной команде панели должен бросить');
|
||||
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
|
||||
|
||||
err = null;
|
||||
try { await clickElement('Недоступная кнопка'); } catch (e) { err = e; }
|
||||
assert.ok(err, 'клик по недоступной обычной кнопке (frameButton) должен бросить');
|
||||
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
|
||||
|
||||
err = null;
|
||||
try { await clickElement('Флажок недоступный'); } catch (e) { err = e; }
|
||||
assert.ok(err, 'клик по недоступному флажку должен бросить');
|
||||
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
|
||||
|
||||
// Позитив: доступная кнопка кликается штатно (регресс — гард не ложно-срабатывает).
|
||||
const r = await clickElement('Доступная команда');
|
||||
assert.ok(r.clicked, 'доступная команда → clicked без ошибки');
|
||||
});
|
||||
|
||||
await step('fillFields бросает на недоступном поле и флажке, проходит на доступном', async () => {
|
||||
let err = null;
|
||||
try { await fillFields({ 'Строка недоступная': 'тест' }); } catch (e) { err = e; }
|
||||
assert.ok(err, 'заполнение недоступного поля должно бросить');
|
||||
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
|
||||
|
||||
err = null;
|
||||
try { await fillFields({ 'Флажок недоступный': 'true' }); } catch (e) { err = e; }
|
||||
assert.ok(err, 'toggle недоступного флажка должен бросить');
|
||||
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
|
||||
|
||||
// Позитив: доступное поле заполняется штатно.
|
||||
const r = await fillFields({ 'Строка доступная': 'привет' });
|
||||
assert.ok(r.filled?.some(f => f.ok), 'доступное поле заполнено');
|
||||
});
|
||||
|
||||
await step('selectValue бросает на недоступном ссылочном поле, проходит на доступном', async () => {
|
||||
let err = null;
|
||||
try { await selectValue('Контрагент недоступный', 'ООО Север'); } catch (e) { err = e; }
|
||||
assert.ok(err, 'selectValue по недоступному ссыл. полю должен бросить');
|
||||
assert.includes(err.message, 'is disabled', 'сообщение "is disabled"');
|
||||
|
||||
// Позитив: доступное ссылочное поле выбирается штатно.
|
||||
const r = await selectValue('Контрагент доступный', 'ООО Север');
|
||||
assert.ok(r.selected, 'доступное ссыл. поле — selected');
|
||||
|
||||
await closeForm({ save: false });
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,7 @@ E2E-тесты движка `web-test` (Playwright + изолированная
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
# Полный регресс (все 25 тестов; фикстура _hang/ в него не входит — см. ниже)
|
||||
# Полный регресс (фикстура _hang/ в него не входит — см. ниже)
|
||||
node .claude/skills/web-test/scripts/run.mjs test tests/web-test/
|
||||
|
||||
# Один файл
|
||||
|
||||
Reference in New Issue
Block a user