mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-19 00:59:40 +03:00
feat(web-test): closeForm закрывает форму каскадом, кнопка подтверждения по смыслу
closeForm умел только Escape. На реальном стенде Escape не закрывает ни
модальную форму, ни даже обычный список — форма оставалась открытой, и
resetState считал контекст грязным.
- Каскад закрытия (closeCrossScript в dom/forms.mjs): Escape → крестик
плавающего окна (ps<N>, модалка поверх формы) → крестик страницы формы
(VW_page<N>, работает и при скрытой панели вкладок) → крестик активной
вкладки (класс select, не первый попавшийся .openedClose). Порядок и якоря
замерены живьём. Крестик формы бьёт вкладочный, потому что панель открытых
может быть выключена настройкой.
- nothingToClose: когда Escape не помог и крестика нет нигде — платформа сама
говорит, что поверхность не закрывается, то есть это рабочий стол. Этот сигнал
и питает «чисто» в resetState, без базового снимка и списков-исключений.
- Семантика подтверждения по смыслу вопроса (pickConfirmationLabel): 1С теми же
кнопками «Да/Нет» задаёт разные вопросы. «Сохранить изменения?» → save?Да:Нет
(как было); «Закрыть согласование?» → Да=закрыть (иначе save:false жал «Нет» =
«остаться», и модалка утекала в следующий тест). Решение по вопросительному
предложению; неизвестная формулировка → прежнее поведение.
- Ответственность DOM: поиск крестика вынесен в dom/forms.mjs (генератор скрипта
для page.evaluate), close.mjs его только зовёт. Диагностика приведена к
английскому, как остальной модуль; русским остаётся лишь цитата платформы.
Проверено на свежей базе пилота: модалка «Комментарий и согласование» →
closeForm({save:false}) = closed:true, answered:Да, viaCross:true, formCount 2→1
(критерий пилота); стопка документ→список→стол разбирается насквозь и
останавливается на столе. Полный регресс 25/25, closeForm зовёт каждый тест.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom v1.16 — facade re-exporting injectable DOM scripts from dom/
|
// web-test dom v1.18 — facade re-exporting injectable DOM scripts from dom/
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* Facade: re-exports DOM selector & semantic mapping script generators.
|
* Facade: re-exports DOM selector & semantic mapping script generators.
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
detectFormScript,
|
detectFormScript,
|
||||||
|
closeCrossScript,
|
||||||
readFormScript,
|
readFormScript,
|
||||||
findClickTargetScript,
|
findClickTargetScript,
|
||||||
findFieldButtonScript,
|
findFieldButtonScript,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom/forms v1.7 — form detection, content read, click-target/field-button resolution
|
// web-test dom/forms v1.9 — 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 } from './_shared.mjs';
|
||||||
|
|
||||||
@@ -13,6 +13,44 @@ export function detectFormScript() {
|
|||||||
})()`;
|
})()`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the cross that closes the topmost closable surface, or null when there is nothing to
|
||||||
|
* close — which is exactly how the platform says "this is the desktop": the home page has no
|
||||||
|
* cross at all. That `null` is the reset-between-tests "clean" signal; no baseline snapshot and
|
||||||
|
* no exception list needed. Measured on a live stand:
|
||||||
|
* desktop, 1 form → form=0 formCount=1, no cross
|
||||||
|
* desktop, 3 forms → form=5 formCount=3 openForms=[5,6,7], no cross
|
||||||
|
* list open → cross VW_page2headerTopLine_cmd_CloseButton
|
||||||
|
*
|
||||||
|
* The id must leak out for the caller to click — Escape is not enough: on a real stand it closed
|
||||||
|
* neither a modal nor even a plain list.
|
||||||
|
*
|
||||||
|
* Order matters, and each step was measured:
|
||||||
|
* 1. floating window (`ps<N>`) — a modal sits ON TOP of the form that opened it, so its cross
|
||||||
|
* wins. The index grows per window (ps0 → ps1 → …): a literal `ps0` breaks on the second
|
||||||
|
* modal of a session.
|
||||||
|
* 2. the form's own page cross — works whether or not the tab panel is switched on, which is
|
||||||
|
* why it beats the tab cross. With the panel hidden it is the ONLY path.
|
||||||
|
* 3. the ACTIVE tab's cross — last resort. `select` marks the active tab (same signal as
|
||||||
|
* dom/form-state.mjs); NOT `querySelector('.openedClose')` like closeModals does — that
|
||||||
|
* grabs the first tab in the DOM and would close someone else's form.
|
||||||
|
* Anchored on ids rather than title="Закрыть", so a non-Russian locale keeps working.
|
||||||
|
*/
|
||||||
|
export function closeCrossScript() {
|
||||||
|
return `(() => {
|
||||||
|
const vis = (e) => e.offsetWidth > 0;
|
||||||
|
const crosses = [...document.querySelectorAll('[id*="headerTopLine_cmd_CloseButton"]')].filter(vis);
|
||||||
|
const floating = crosses.filter(e => /ps\\d+headerTopLine_cmd_CloseButton$/.test(e.id));
|
||||||
|
if (floating.length) return floating.pop().id;
|
||||||
|
const page = crosses.filter(e => /^VW_page\\d+headerTopLine_cmd_CloseButton$/.test(e.id));
|
||||||
|
if (page.length) return page.pop().id;
|
||||||
|
const tab = [...document.querySelectorAll('[id^="openedCell_cmd_"]')]
|
||||||
|
.find(e => e.classList.contains('select') && vis(e));
|
||||||
|
const tabCross = tab && tab.querySelector('.openedClose');
|
||||||
|
return (tabCross && vis(tabCross)) ? tabCross.id : null;
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read full form state for a given form number.
|
* Read full form state for a given form number.
|
||||||
* Uses shared READ_FORM_FN.
|
* Uses shared READ_FORM_FN.
|
||||||
|
|||||||
@@ -1,19 +1,40 @@
|
|||||||
// web-test forms/close v1.18 — Close current form via Escape, handle save-changes confirmation.
|
// web-test forms/close v1.20 — Close current form via Escape, handle save-changes confirmation.
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import { page, recorder, ensureConnected } from '../core/state.mjs';
|
import { page, recorder, ensureConnected } from '../core/state.mjs';
|
||||||
import { detectFormScript } from '../../dom.mjs';
|
import { detectFormScript, closeCrossScript } from '../../dom.mjs';
|
||||||
import { dismissPendingErrors, checkForErrors, detectPlatformDialogs, closePlatformDialogs } from '../core/errors.mjs';
|
import { dismissPendingErrors, checkForErrors, detectPlatformDialogs, closePlatformDialogs } from '../core/errors.mjs';
|
||||||
import { waitForStable } from '../core/wait.mjs';
|
import { waitForStable } from '../core/wait.mjs';
|
||||||
import { returnFormState } from '../core/helpers.mjs';
|
import { returnFormState } from '../core/helpers.mjs';
|
||||||
import { getFormState } from './state.mjs';
|
import { getFormState } from './state.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the current form/dialog via Escape.
|
* Which button actually CLOSES the form on this confirmation?
|
||||||
|
*
|
||||||
|
* 1C asks two different questions with the same two buttons:
|
||||||
|
* «Данные были изменены. Сохранить изменения?» → Да = save+close, Нет = close without saving
|
||||||
|
* «Виза сохранена не будет. Закрыть согласование?» → Да = close, Нет = STAY IN THE FORM
|
||||||
|
* A hard-coded «Нет» for save:false is right for the first and exactly backwards for the second:
|
||||||
|
* the caller asks to close and gets a form that stays open (measured — that is how a modal leaked
|
||||||
|
* into the next test).
|
||||||
|
*
|
||||||
|
* Decide by the QUESTION, not by the whole message: both texts contain the root «сохран», but only
|
||||||
|
* the interrogative sentence says what the buttons mean. Unknown wording keeps the legacy answer.
|
||||||
|
*/
|
||||||
|
function pickConfirmationLabel(message, save) {
|
||||||
|
const question = (String(message || '').match(/[^.!?]*\?/g) || []).pop() || '';
|
||||||
|
if (/сохранит/i.test(question)) return save ? 'Да' : 'Нет'; // "…Сохранить изменения?"
|
||||||
|
if (/закрыт/i.test(question)) return 'Да'; // "…Закрыть согласование?" — Да = закрыть
|
||||||
|
return save ? 'Да' : 'Нет'; // unknown → as before
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the current form/dialog: Escape first, the modal window cross if Escape does nothing.
|
||||||
* @param {Object} [opts]
|
* @param {Object} [opts]
|
||||||
* @param {boolean} [opts.save] - Handle "Save changes?" confirmation automatically:
|
* @param {boolean} [opts.save] - Handle a confirmation automatically. The button is chosen by the
|
||||||
* true → click "Да" (save and close)
|
* MEANING of the question (see pickConfirmationLabel), not by a fixed label:
|
||||||
* false → click "Нет" (discard and close)
|
* true → save and close
|
||||||
|
* false → close without saving
|
||||||
* undefined → return confirmation as hint for caller to decide
|
* undefined → return confirmation as hint for caller to decide
|
||||||
*/
|
*/
|
||||||
export async function closeForm({ save } = {}) {
|
export async function closeForm({ save } = {}) {
|
||||||
@@ -29,11 +50,33 @@ export async function closeForm({ save } = {}) {
|
|||||||
const beforeForm = await page.evaluate(detectFormScript());
|
const beforeForm = await page.evaluate(detectFormScript());
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await waitForStable(beforeForm);
|
await waitForStable(beforeForm);
|
||||||
const state = await getFormState();
|
let state = await getFormState();
|
||||||
const err = await checkForErrors();
|
let err = await checkForErrors();
|
||||||
|
let usedCross = false;
|
||||||
|
let nothingToClose = false;
|
||||||
|
|
||||||
|
// Escape did nothing and raised no question. On a real stand that is the norm, not the exception:
|
||||||
|
// Escape closed neither a modal nor even a plain list there. Fall back to the cross a human would
|
||||||
|
// click. Only when nothing moved, so ordinary forms keep the cheap Escape path.
|
||||||
|
if (!err?.confirmation && state.form === beforeForm) {
|
||||||
|
const crossId = await page.evaluate(closeCrossScript());
|
||||||
|
if (crossId) {
|
||||||
|
await page.click(`#${crossId}`).catch(() => {});
|
||||||
|
await waitForStable(beforeForm);
|
||||||
|
state = await getFormState();
|
||||||
|
err = await checkForErrors();
|
||||||
|
usedCross = true;
|
||||||
|
} else {
|
||||||
|
// No cross anywhere: the platform itself says this surface is not closable — i.e. we are on
|
||||||
|
// the desktop. This is what tells resetState "clean" without knowing anything about the
|
||||||
|
// application: a home page with three forms on it looks exactly like an empty one here.
|
||||||
|
nothingToClose = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (err?.confirmation) {
|
if (err?.confirmation) {
|
||||||
if (save === true || save === false) {
|
if (save === true || save === false) {
|
||||||
const label = save ? 'Да' : 'Нет';
|
const label = pickConfirmationLabel(err.confirmation.message, save);
|
||||||
const btnSel = `#form${err.confirmation.formNum}_container a.press.pressButton`;
|
const btnSel = `#form${err.confirmation.formNum}_container a.press.pressButton`;
|
||||||
const btns = await page.$$(btnSel);
|
const btns = await page.$$(btnSel);
|
||||||
for (const b of btns) {
|
for (const b of btns) {
|
||||||
@@ -46,11 +89,17 @@ export async function closeForm({ save } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const afterForm = await page.evaluate(detectFormScript());
|
const afterForm = await page.evaluate(detectFormScript());
|
||||||
return returnFormState({ closed: afterForm !== beforeForm });
|
// Report which button was pressed: on a "…Закрыть?" question it is «Да» even for save:false,
|
||||||
|
// and a silent surprise there is what cost the last investigation its afternoon.
|
||||||
|
return returnFormState({ closed: afterForm !== beforeForm, confirmationAnswered: label, closedViaCross: usedCross || undefined });
|
||||||
}
|
}
|
||||||
state.confirmation = err.confirmation;
|
state.confirmation = err.confirmation;
|
||||||
state.hint = 'Confirmation dialog shown. Click "Да" to confirm or "Нет" to cancel';
|
state.hint = 'Confirmation dialog shown. Click "Да" to confirm or "Нет" to cancel';
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
return returnFormState({ closed: state.form !== beforeForm });
|
return returnFormState({
|
||||||
|
closed: state.form !== beforeForm,
|
||||||
|
closedViaCross: usedCross || undefined,
|
||||||
|
nothingToClose: nothingToClose || undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user