mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-20 01:20:59 +03:00
refactor(web-test): извлечены DOM-скрипты dialog/picker UI из select-value
Применены shared-функции из S2 (findSearchInputScript, findNamedButton, findCompareTypeRadio, isFormVisible). Добавлены новые для type-dialog и picker UI: - findPatternInputIdScript — Pattern input id (Alt+F dialog) - isTypeDialogScript — OK + ValueList + "Выбор типа" title - isNotInListCloudVisibleScript — "нет в списке" tooltip popup - findChildFormByButtonScript — поиск child-form по имени кнопки - readTypeDialogVisibleRowsScript — visible rows + fuzzy matches в ValueList select-value.mjs: 950 → 880 LOC (−70), inline page.evaluate 24 → 7 (планировали ≤8). Регресс 06/11/13 зелёный; полный регресс зелёный (Checkpoint-1 пройден). 04-selectvalue auto-history шаг — pre-existing test-isolation issue (см. S1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7f7ab2f217
commit
340142b0a2
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom v1.10 — facade re-exporting injectable DOM scripts from dom/
|
// web-test dom v1.11 — 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.
|
||||||
@@ -20,6 +20,11 @@ export {
|
|||||||
findNamedButtonScript,
|
findNamedButtonScript,
|
||||||
findCompareTypeRadioScript,
|
findCompareTypeRadioScript,
|
||||||
isFormVisibleScript,
|
isFormVisibleScript,
|
||||||
|
findPatternInputIdScript,
|
||||||
|
isTypeDialogScript,
|
||||||
|
isNotInListCloudVisibleScript,
|
||||||
|
findChildFormByButtonScript,
|
||||||
|
readTypeDialogVisibleRowsScript,
|
||||||
} from './dom/forms.mjs';
|
} from './dom/forms.mjs';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom/forms v1.2 — form detection, content read, click-target/field-button resolution
|
// web-test dom/forms v1.3 — 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 } from './_shared.mjs';
|
import { DETECT_FORM_FN, READ_FORM_FN } from './_shared.mjs';
|
||||||
|
|
||||||
@@ -483,3 +483,102 @@ export function isFormVisibleScript(dialogForm) {
|
|||||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
||||||
})()`;
|
})()`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the Pattern input id on a search/filter dialog. Returns `id | null`.
|
||||||
|
*/
|
||||||
|
export function findPatternInputIdScript(dialogForm) {
|
||||||
|
return `(() => {
|
||||||
|
const p = 'form${dialogForm}_';
|
||||||
|
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
||||||
|
.find(el => el.offsetWidth > 0 && /Pattern/i.test(el.id));
|
||||||
|
return el ? el.id : null;
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the given form a type selection dialog ("Выбор типа данных")?
|
||||||
|
*
|
||||||
|
* Detection signals (any one is sufficient):
|
||||||
|
* - `form{N}_OK` element exists (selection forms use "Выбрать", not "OK")
|
||||||
|
* - `form{N}_ValueList` grid exists (specific to type/value list dialogs)
|
||||||
|
* - window title contains "Выбор типа" on a visible `.toplineBoxTitle`
|
||||||
|
*
|
||||||
|
* Returns boolean.
|
||||||
|
*/
|
||||||
|
export function isTypeDialogScript(formNum) {
|
||||||
|
return `(() => {
|
||||||
|
const p = 'form' + ${formNum} + '_';
|
||||||
|
const hasOK = !!document.getElementById(p + 'OK');
|
||||||
|
const hasValueList = !!document.getElementById(p + 'ValueList');
|
||||||
|
const hasTitle = [...document.querySelectorAll('.toplineBoxTitle')]
|
||||||
|
.some(el => el.offsetWidth > 0 && /выбор типа/i.test(el.getAttribute('title') || ''));
|
||||||
|
return hasOK || hasValueList || hasTitle;
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the "нет в списке" cloud popup visible? 1C shows it as a positioned div
|
||||||
|
* (absolute/fixed, high z-index) whose text contains "нет в списке".
|
||||||
|
* Returns boolean.
|
||||||
|
*/
|
||||||
|
export function isNotInListCloudVisibleScript() {
|
||||||
|
return `(() => {
|
||||||
|
const divs = document.querySelectorAll('div');
|
||||||
|
for (const el of divs) {
|
||||||
|
if (el.offsetWidth === 0 || el.offsetHeight === 0) continue;
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
if (style.position !== 'absolute' && style.position !== 'fixed') continue;
|
||||||
|
const z = parseInt(style.zIndex) || 0;
|
||||||
|
if (z < 100) continue;
|
||||||
|
if ((el.innerText || '').includes('нет в списке')) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a child form opened above `prevFormNum` whose `form{N}_{buttonName}` button is visible.
|
||||||
|
* Used by type-dialog Ctrl+F flow to locate the "Найти" sub-dialog form number.
|
||||||
|
* Returns the form number or `null`.
|
||||||
|
*/
|
||||||
|
export function findChildFormByButtonScript(prevFormNum, buttonName, range = 20) {
|
||||||
|
return `(() => {
|
||||||
|
for (let n = ${prevFormNum} + 1; n < ${prevFormNum} + ${range}; n++) {
|
||||||
|
const btn = document.getElementById('form' + n + '_' + ${JSON.stringify(buttonName)});
|
||||||
|
if (btn && btn.offsetWidth > 0) return n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read visible rows of a type-dialog ValueList grid and return rows that fuzzy-match `typeNorm`.
|
||||||
|
*
|
||||||
|
* `typeNorm` should already be lowercased, NBSP-normalized, ё→е normalized (use `normYo`).
|
||||||
|
*
|
||||||
|
* Returns `{ visible: string[], matches: Array<{ text, x, y }> }`.
|
||||||
|
*/
|
||||||
|
export function readTypeDialogVisibleRowsScript(formNum, typeNorm) {
|
||||||
|
return `(() => {
|
||||||
|
const grid = document.getElementById('form${formNum}_ValueList');
|
||||||
|
if (!grid) return { visible: [], matches: [] };
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (!body) return { visible: [], matches: [] };
|
||||||
|
const lines = body.querySelectorAll('.gridLine');
|
||||||
|
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim();
|
||||||
|
const typeNorm = ${JSON.stringify(typeNorm)};
|
||||||
|
const visible = [];
|
||||||
|
const matches = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const text = norm(line.innerText);
|
||||||
|
if (!text) continue;
|
||||||
|
visible.push(text);
|
||||||
|
if (text.toLowerCase().replace(/ё/gi, 'е').includes(typeNorm)) {
|
||||||
|
const r = line.getBoundingClientRect();
|
||||||
|
matches.push({ text, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { visible, matches };
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// web-test forms/select-value v1.16 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
|
// web-test forms/select-value v1.17 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers.
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
detectFormScript, findFieldButtonScript, resolveFieldsScript,
|
detectFormScript, findFieldButtonScript, resolveFieldsScript,
|
||||||
readSubmenuScript, checkErrorsScript,
|
readSubmenuScript, checkErrorsScript,
|
||||||
|
findSearchInputScript, findNamedButtonScript, findCompareTypeRadioScript, isFormVisibleScript,
|
||||||
|
findPatternInputIdScript, isTypeDialogScript, isNotInListCloudVisibleScript,
|
||||||
|
findChildFormByButtonScript, readTypeDialogVisibleRowsScript,
|
||||||
} from '../../dom.mjs';
|
} from '../../dom.mjs';
|
||||||
import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs';
|
import { dismissPendingErrors, checkForErrors } from '../core/errors.mjs';
|
||||||
import { waitForStable, waitForCondition } from '../core/wait.mjs';
|
import { waitForStable, waitForCondition } from '../core/wait.mjs';
|
||||||
@@ -68,10 +71,7 @@ async function dblclickAndVerify(coords, selFormNum, fieldName) {
|
|||||||
await waitForStable(selFormNum);
|
await waitForStable(selFormNum);
|
||||||
|
|
||||||
// Verify selection form closed
|
// Verify selection form closed
|
||||||
const stillOpen = await page.evaluate(`(() => {
|
const stillOpen = await page.evaluate(isFormVisibleScript(selFormNum));
|
||||||
const p = 'form${selFormNum}_';
|
|
||||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
|
||||||
})()`);
|
|
||||||
if (stillOpen) {
|
if (stillOpen) {
|
||||||
// Enter didn't select — item is likely a non-selectable group.
|
// Enter didn't select — item is likely a non-selectable group.
|
||||||
// Don't Escape here — let the caller decide (may want to try another row).
|
// Don't Escape here — let the caller decide (may want to try another row).
|
||||||
@@ -105,26 +105,14 @@ async function advancedSearchInline(formNum, text) {
|
|||||||
if (dialogForm === formNum || dialogForm === null) return; // Alt+F didn't open dialog
|
if (dialogForm === formNum || dialogForm === null) return; // Alt+F didn't open dialog
|
||||||
|
|
||||||
// 2. Switch to "по части строки" (CompareType#1)
|
// 2. Switch to "по части строки" (CompareType#1)
|
||||||
const radioClicked = await page.evaluate(`(() => {
|
const radioClicked = await page.evaluate(findCompareTypeRadioScript(dialogForm, 1));
|
||||||
const p = 'form${dialogForm}_';
|
if (radioClicked && !radioClicked.already) {
|
||||||
const el = document.getElementById(p + 'CompareType#1#radio');
|
|
||||||
if (!el || el.offsetWidth === 0) return false;
|
|
||||||
if (el.classList.contains('select')) return true; // already selected
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
||||||
})()`);
|
|
||||||
if (radioClicked && typeof radioClicked === 'object') {
|
|
||||||
await page.mouse.click(radioClicked.x, radioClicked.y);
|
await page.mouse.click(radioClicked.x, radioClicked.y);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fill Pattern field via clipboard paste
|
// 3. Fill Pattern field via clipboard paste
|
||||||
const patternId = await page.evaluate(`(() => {
|
const patternId = await page.evaluate(findPatternInputIdScript(dialogForm));
|
||||||
const p = 'form${dialogForm}_';
|
|
||||||
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
|
||||||
.find(el => el.offsetWidth > 0 && /Pattern/i.test(el.id));
|
|
||||||
return el ? el.id : null;
|
|
||||||
})()`);
|
|
||||||
if (!patternId) {
|
if (!patternId) {
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
@@ -137,13 +125,7 @@ async function advancedSearchInline(formNum, text) {
|
|||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
// 4. Click "Найти"
|
// 4. Click "Найти"
|
||||||
const findBtn = await page.evaluate(`(() => {
|
const findBtn = await page.evaluate(findNamedButtonScript('Найти'));
|
||||||
const btns = [...document.querySelectorAll('a.press')].filter(el => el.offsetWidth > 0);
|
|
||||||
const btn = btns.find(el => el.innerText?.trim() === 'Найти');
|
|
||||||
if (!btn) return null;
|
|
||||||
const r = btn.getBoundingClientRect();
|
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
||||||
})()`);
|
|
||||||
if (findBtn) {
|
if (findBtn) {
|
||||||
await page.mouse.click(findBtn.x, findBtn.y);
|
await page.mouse.click(findBtn.x, findBtn.y);
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
@@ -151,10 +133,7 @@ async function advancedSearchInline(formNum, text) {
|
|||||||
|
|
||||||
// 5. Close advanced search dialog
|
// 5. Close advanced search dialog
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
const dialogVisible = await page.evaluate(`(() => {
|
const dialogVisible = await page.evaluate(isFormVisibleScript(dialogForm));
|
||||||
const p = 'form${dialogForm}_';
|
|
||||||
return [...document.querySelectorAll('[id^="' + p + '"]')].some(el => el.offsetWidth > 0);
|
|
||||||
})()`);
|
|
||||||
if (!dialogVisible) break;
|
if (!dialogVisible) break;
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
@@ -224,15 +203,10 @@ export async function pickFromSelectionForm(selFormNum, fieldName, search, origF
|
|||||||
|
|
||||||
// Step 3: Fallback — simple search via search input (for forms without Alt+F support)
|
// Step 3: Fallback — simple search via search input (for forms without Alt+F support)
|
||||||
if (typeof search === 'string' && searchLower) {
|
if (typeof search === 'string' && searchLower) {
|
||||||
const searchInputId = await page.evaluate(`(() => {
|
const searchInputInfo = await page.evaluate(findSearchInputScript(selFormNum));
|
||||||
const p = 'form${selFormNum}_';
|
if (searchInputInfo) {
|
||||||
const el = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
|
||||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
|
||||||
return el ? el.id : null;
|
|
||||||
})()`);
|
|
||||||
if (searchInputId) {
|
|
||||||
try {
|
try {
|
||||||
await page.click(`[id="${searchInputId}"]`);
|
await page.click(`[id="${searchInputInfo.id}"]`);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await pasteText(searchText);
|
await pasteText(searchText);
|
||||||
@@ -278,14 +252,7 @@ export async function pickFromSelectionForm(selFormNum, fieldName, search, origF
|
|||||||
* - Window title contains "Выбор типа" (title attr on .toplineBoxTitle)
|
* - Window title contains "Выбор типа" (title attr on .toplineBoxTitle)
|
||||||
*/
|
*/
|
||||||
export async function isTypeDialog(formNum) {
|
export async function isTypeDialog(formNum) {
|
||||||
return page.evaluate(`(() => {
|
return page.evaluate(isTypeDialogScript(formNum));
|
||||||
const p = 'form' + ${formNum} + '_';
|
|
||||||
const hasOK = !!document.getElementById(p + 'OK');
|
|
||||||
const hasValueList = !!document.getElementById(p + 'ValueList');
|
|
||||||
const hasTitle = [...document.querySelectorAll('.toplineBoxTitle')]
|
|
||||||
.some(el => el.offsetWidth > 0 && /выбор типа/i.test(el.getAttribute('title') || ''));
|
|
||||||
return hasOK || hasValueList || hasTitle;
|
|
||||||
})()`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,27 +284,7 @@ export async function pickFromTypeDialog(formNum, typeName) {
|
|||||||
|
|
||||||
// Helper: read visible rows and find matching ones
|
// Helper: read visible rows and find matching ones
|
||||||
async function readVisibleRows() {
|
async function readVisibleRows() {
|
||||||
return page.evaluate(`(() => {
|
return page.evaluate(readTypeDialogVisibleRowsScript(formNum, typeNorm));
|
||||||
const grid = document.getElementById('form${formNum}_ValueList');
|
|
||||||
if (!grid) return { visible: [], matches: [] };
|
|
||||||
const body = grid.querySelector('.gridBody');
|
|
||||||
if (!body) return { visible: [], matches: [] };
|
|
||||||
const lines = body.querySelectorAll('.gridLine');
|
|
||||||
const norm = s => (s || '').replace(/\\u00a0/g, ' ').trim();
|
|
||||||
const typeNorm = ${JSON.stringify(typeNorm)};
|
|
||||||
const visible = [];
|
|
||||||
const matches = [];
|
|
||||||
for (const line of lines) {
|
|
||||||
const text = norm(line.innerText);
|
|
||||||
if (!text) continue;
|
|
||||||
visible.push(text);
|
|
||||||
if (text.toLowerCase().replace(/ё/gi, 'е').includes(typeNorm)) {
|
|
||||||
const r = line.getBoundingClientRect();
|
|
||||||
matches.push({ text, x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { visible, matches };
|
|
||||||
})()`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Scan visible rows (fast path — no Ctrl+F needed for small lists)
|
// Step 1: Scan visible rows (fast path — no Ctrl+F needed for small lists)
|
||||||
@@ -379,13 +326,7 @@ export async function pickFromTypeDialog(formNum, typeName) {
|
|||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
// Find the "Найти" dialog form number (it's > formNum)
|
// Find the "Найти" dialog form number (it's > formNum)
|
||||||
const findFormNum = await page.evaluate(`(() => {
|
const findFormNum = await page.evaluate(findChildFormByButtonScript(formNum, 'Find'));
|
||||||
for (let n = ${formNum} + 1; n < ${formNum} + 20; n++) {
|
|
||||||
const btn = document.getElementById('form' + n + '_Find');
|
|
||||||
if (btn && btn.offsetWidth > 0) return n;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})()`);
|
|
||||||
|
|
||||||
if (findFormNum === null) {
|
if (findFormNum === null) {
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
@@ -455,18 +396,7 @@ export async function fillReferenceField(selector, fieldName, value, formNum) {
|
|||||||
|
|
||||||
// Helper: check for "not in list" cloud popup (1C shows positioned div with "нет в списке")
|
// Helper: check for "not in list" cloud popup (1C shows positioned div with "нет в списке")
|
||||||
async function checkNotInListCloud() {
|
async function checkNotInListCloud() {
|
||||||
return page.evaluate(`(() => {
|
return page.evaluate(isNotInListCloudVisibleScript());
|
||||||
const divs = document.querySelectorAll('div');
|
|
||||||
for (const el of divs) {
|
|
||||||
if (el.offsetWidth === 0 || el.offsetHeight === 0) continue;
|
|
||||||
const style = getComputedStyle(el);
|
|
||||||
if (style.position !== 'absolute' && style.position !== 'fixed') continue;
|
|
||||||
const z = parseInt(style.zIndex) || 0;
|
|
||||||
if (z < 100) continue;
|
|
||||||
if ((el.innerText || '').includes('нет в списке')) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})()`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0. Dismiss any leftover error modal from a previous operation
|
// 0. Dismiss any leftover error modal from a previous operation
|
||||||
|
|||||||
Reference in New Issue
Block a user