mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-15 10:24:57 +03:00
feat(web-test): auto-fetch error call stack on 1C exceptions
When a 1C error modal is detected, automatically retrieve the full call
stack before throwing. Uses two strategies: Path 1 clicks the OpenReport
link for platform exceptions, Path 2 navigates hamburger → About →
Support Info for handled ВызватьИсключение errors. The stack is returned
as structured {raw, entries[{location, code}], timestamp} in the error
result. Handles unstable modal redraws with a 1.5s re-check delay.
Platform dialogs are always cleaned up via try/finally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -359,6 +359,237 @@ async function dismissPendingErrors() {
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close any platform-level dialogs that may be left open (about, support info, error report).
|
||||
* These are NOT 1C forms — they are platform UI overlays invisible to getFormState().
|
||||
* Each close is wrapped in try/catch to avoid cascading failures.
|
||||
*/
|
||||
async function _closePlatformDialogs() {
|
||||
await page.evaluate(() => {
|
||||
// "Подробный текст ошибки" OK button (inside error report detail view)
|
||||
// It's a cloud window with its own OK button — look for visible pressDefault in small ps*win
|
||||
const psWins = document.querySelectorAll('[id^="ps"][id$="win"]');
|
||||
for (const w of psWins) {
|
||||
if (w.offsetWidth === 0) continue;
|
||||
// Check if this is a small dialog (error detail, about, support info)
|
||||
const closeBtn = w.querySelector('[id$="_cmd_CloseButton"]');
|
||||
if (closeBtn && closeBtn.offsetWidth > 0) {
|
||||
try { closeBtn.click(); } catch {}
|
||||
}
|
||||
}
|
||||
// "Информация для технической поддержки" — extOkBtn
|
||||
const extOk = document.getElementById('extOkBtn');
|
||||
if (extOk && extOk.offsetWidth > 0) try { extOk.click(); } catch {}
|
||||
// "О программе" — aboutOkButton
|
||||
const aboutOk = document.getElementById('aboutOkButton');
|
||||
if (aboutOk && aboutOk.offsetWidth > 0) try { aboutOk.click(); } catch {}
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw error stack text into structured entries.
|
||||
* Input: raw text from errJournalInput (first block) or "Подробный текст ошибки" textarea.
|
||||
* Returns { raw, timestamp?, entries: [{location, code}] }
|
||||
*/
|
||||
function _parseErrorStack(raw) {
|
||||
if (!raw) return null;
|
||||
const result = { raw, entries: [] };
|
||||
// Extract timestamp if present (format: DD.MM.YYYY HH:MM:SS)
|
||||
const tsMatch = raw.match(/^(\d{2}\.\d{2}\.\d{4}\s+\d{1,2}:\d{2}:\d{2})/m);
|
||||
if (tsMatch) result.timestamp = tsMatch[1];
|
||||
// Extract {Module.Path(lineNum)}: code entries
|
||||
const entryRe = /\{([^}]+)\}:\s*(.+)/g;
|
||||
let m;
|
||||
while ((m = entryRe.exec(raw)) !== null) {
|
||||
result.entries.push({ location: m[1].trim(), code: m[2].trim() });
|
||||
}
|
||||
return result.entries.length > 0 ? result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch error call stack from the 1C platform UI.
|
||||
* Uses two strategies:
|
||||
* Path 1 (hasReport=true): Click OpenReport link → "подробный текст ошибки" → read textarea
|
||||
* Path 2 (fallback): Hamburger → "О программе" → "Информация для техподдержки" → errJournalInput
|
||||
*
|
||||
* Always closes the error modal and any platform dialogs it opened.
|
||||
* Returns parsed stack object or null on failure.
|
||||
*
|
||||
* @param {number} formNum - form number of the error modal (e.g. 6 for form6_)
|
||||
* @param {boolean} hasReport - true if OpenReport link is available
|
||||
*/
|
||||
export async function fetchErrorStack(formNum, hasReport) {
|
||||
try {
|
||||
// Platform exception modals are initially unstable — they redraw within ~1s.
|
||||
// The initial state may lack the OpenReport link. Re-check after a short delay.
|
||||
if (!hasReport) {
|
||||
await page.waitForTimeout(1500);
|
||||
hasReport = await page.evaluate((fn) => {
|
||||
const el = document.getElementById('form' + fn + '_OpenReport#text');
|
||||
return !!(el && el.offsetWidth > 2 && el.textContent.trim());
|
||||
}, formNum);
|
||||
}
|
||||
if (hasReport) return await _fetchStackViaReport(formNum);
|
||||
return await _fetchStackViaHamburger(formNum);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
// Ensure all platform dialogs are closed
|
||||
try { await _closePlatformDialogs(); } catch {}
|
||||
// Ensure the error modal itself is closed
|
||||
try {
|
||||
const sel = formNum != null
|
||||
? `#form${formNum}_container a.press.pressDefault`
|
||||
: 'a.press.pressDefault';
|
||||
const btn = await page.$(sel);
|
||||
if (btn) await btn.click({ force: true });
|
||||
await page.waitForTimeout(300);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Path 1: Fetch stack via OpenReport link (for platform exceptions).
|
||||
* The error modal must still be open with a visible "Сформировать отчет об ошибке" link.
|
||||
*/
|
||||
async function _fetchStackViaReport(formNum) {
|
||||
// 1. Get coordinates of the OpenReport link and click via mouse (modalSurface blocks JS clicks)
|
||||
const coords = await page.evaluate((fn) => {
|
||||
const el = document.getElementById('form' + fn + '_OpenReport#text');
|
||||
if (!el || el.offsetWidth <= 2) return null;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
||||
}, formNum);
|
||||
if (!coords) return null;
|
||||
|
||||
await page.mouse.click(coords.x, coords.y);
|
||||
|
||||
// 2. Wait for "Отчет об ошибке" dialog — look for "подробный текст ошибки" link
|
||||
let found = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await page.waitForTimeout(500);
|
||||
found = await page.evaluate(() => {
|
||||
const links = document.querySelectorAll('a, [class*="hyper"], span');
|
||||
for (const el of links) {
|
||||
if (el.offsetWidth > 0 && el.textContent.includes('подробный текст ошибки')) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (found) break;
|
||||
}
|
||||
if (!found) return null;
|
||||
|
||||
// 3. Click "подробный текст ошибки"
|
||||
await page.getByText('подробный текст ошибки').click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 4. Read the textarea with detailed error text (find the largest visible textarea)
|
||||
const raw = await page.evaluate(() => {
|
||||
let best = null;
|
||||
document.querySelectorAll('textarea').forEach(ta => {
|
||||
if (ta.offsetWidth > 0 && ta.value.length > 0) {
|
||||
if (!best || ta.value.length > best.value.length) best = ta;
|
||||
}
|
||||
});
|
||||
return best?.value || null;
|
||||
});
|
||||
|
||||
// 5. Close "Подробный текст ошибки" dialog (click its OK button)
|
||||
try {
|
||||
const okBtn = await page.evaluate(() => {
|
||||
// Find the OK button in the topmost small cloud window
|
||||
const psWins = [...document.querySelectorAll('[id^="ps"][id$="win"]')]
|
||||
.filter(w => w.offsetWidth > 0)
|
||||
.sort((a, b) => parseInt(b.style?.zIndex || '0') - parseInt(a.style?.zIndex || '0'));
|
||||
for (const w of psWins) {
|
||||
const ok = w.querySelector('button.webBtn, .pressDefault');
|
||||
if (ok && ok.textContent.trim() === 'OK') { ok.click(); return true; }
|
||||
}
|
||||
return false;
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
} catch {}
|
||||
|
||||
// 6. Close "Отчет об ошибке" dialog (click its × close button)
|
||||
try {
|
||||
await page.evaluate(() => {
|
||||
const psWins = [...document.querySelectorAll('[id^="ps"][id$="win"]')]
|
||||
.filter(w => w.offsetWidth > 0);
|
||||
for (const w of psWins) {
|
||||
const closeBtn = w.querySelector('[id$="_cmd_CloseButton"]');
|
||||
if (closeBtn && closeBtn.offsetWidth > 0) { closeBtn.click(); break; }
|
||||
}
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
} catch {}
|
||||
|
||||
return _parseErrorStack(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Path 2: Fetch stack via hamburger menu → "О программе" → "Информация для техподдержки".
|
||||
* Works for all error types including simple ВызватьИсключение.
|
||||
* The error modal is closed first to allow access to the hamburger menu.
|
||||
*/
|
||||
async function _fetchStackViaHamburger(formNum) {
|
||||
// 1. Close the error modal first
|
||||
try {
|
||||
const sel = formNum != null
|
||||
? `#form${formNum}_container a.press.pressDefault`
|
||||
: 'a.press.pressDefault';
|
||||
const btn = await page.$(sel);
|
||||
if (btn) await btn.click({ force: true });
|
||||
await page.waitForTimeout(500);
|
||||
} catch {}
|
||||
|
||||
// 2. Click hamburger menu
|
||||
await page.click('#captionbarMore', { timeout: 5000 });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 3. Click "О программе..."
|
||||
await page.getByText('О программе...', { exact: true }).click({ timeout: 5000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 4. Click "Информация для технической поддержки"
|
||||
await page.click('#aboutHyperLink', { timeout: 5000 });
|
||||
|
||||
// 5. Wait for errJournalInput to appear and be filled
|
||||
let raw = null;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await page.waitForTimeout(500);
|
||||
raw = await page.evaluate(() => {
|
||||
const el = document.getElementById('errJournalInput');
|
||||
return (el && el.offsetWidth > 0 && el.value.length > 50) ? el.value : null;
|
||||
});
|
||||
if (raw) break;
|
||||
}
|
||||
if (!raw) return null;
|
||||
|
||||
// 6. Parse first error block (most recent — before first separator)
|
||||
const separator = / - - - - /;
|
||||
const errSection = raw.indexOf('\n\n') !== -1 ? raw.substring(raw.indexOf('\n\n')) : raw;
|
||||
// Find the "Ошибки:" section
|
||||
const errIdx = raw.indexOf('Ошибки:');
|
||||
let errorText = errIdx !== -1 ? raw.substring(errIdx + 'Ошибки:'.length).trim() : raw;
|
||||
// Take first block (before first separator line)
|
||||
const lines = errorText.split('\n');
|
||||
const firstBlockLines = [];
|
||||
let inBlock = false;
|
||||
for (const line of lines) {
|
||||
if (separator.test(line)) {
|
||||
if (inBlock) break; // end of first block
|
||||
inBlock = true;
|
||||
continue;
|
||||
}
|
||||
if (inBlock) firstBlockLines.push(line);
|
||||
}
|
||||
const firstBlock = firstBlockLines.join('\n').trim();
|
||||
|
||||
// 7. Close support info and about dialogs (done in finally via _closePlatformDialogs)
|
||||
return _parseErrorStack(firstBlock || errorText);
|
||||
}
|
||||
|
||||
/** Get the raw Playwright page object (for advanced scripting in skill mode). */
|
||||
export function getPage() {
|
||||
ensureConnected();
|
||||
|
||||
@@ -1147,6 +1147,16 @@ export function checkErrorsScript() {
|
||||
.filter(Boolean);
|
||||
if (texts.length > 0) {
|
||||
result.modal = { message: texts.join(' '), formNum: parseInt(fn), button: buttons[0].innerText?.trim() || '' };
|
||||
// Check if OpenReport link is available (platform exceptions have visible link text)
|
||||
const reportLink = document.getElementById(p + 'OpenReport#text');
|
||||
if (reportLink && reportLink.offsetWidth > 2 && reportLink.textContent.trim()) {
|
||||
result.modal.hasReport = true;
|
||||
}
|
||||
// Grab AdditionalInfo/ServerText if filled (may contain extra error details)
|
||||
const addInfo = document.getElementById(p + 'AdditionalInfo');
|
||||
if (addInfo && addInfo.textContent && addInfo.textContent.trim()) result.modal.additionalInfo = addInfo.textContent.trim();
|
||||
const srvText = document.getElementById(p + 'ServerText');
|
||||
if (srvText && srvText.textContent && srvText.textContent.trim()) result.modal.serverText = srvText.textContent.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,9 +145,16 @@ async function executeScript(code, { noRecord } = {}) {
|
||||
const result = await orig(...args);
|
||||
const errors = result?.errors;
|
||||
if (errors?.modal || errors?.balloon) {
|
||||
// Try to fetch call stack for modal errors before throwing
|
||||
let stack = null;
|
||||
if (errors?.modal && typeof exports.fetchErrorStack === 'function') {
|
||||
try {
|
||||
stack = await exports.fetchErrorStack(errors.modal.formNum, errors.modal.hasReport);
|
||||
} catch { /* don't fail if stack fetch fails */ }
|
||||
}
|
||||
const msg = errors.modal?.message || errors.balloon?.message || 'Unknown 1C error';
|
||||
const err = new Error(msg);
|
||||
err.onecError = { step: name, args, errors, formState: result };
|
||||
err.onecError = { step: name, args, errors, formState: result, stack };
|
||||
throw err;
|
||||
}
|
||||
return result;
|
||||
@@ -186,6 +193,7 @@ async function executeScript(code, { noRecord } = {}) {
|
||||
result.stepArgs = e.onecError.args;
|
||||
result.onecErrors = e.onecError.errors;
|
||||
result.formState = e.onecError.formState;
|
||||
if (e.onecError.stack) result.stack = e.onecError.stack;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user